pull/3331/head
Armağan Ünlü 6 years ago
commit b2e13444cd

@ -81,6 +81,7 @@
"LastSeenDate": "Last seen date",
"{0}Computer{1}WillBeRemovedFromRecords": "Computer of {0} ({1}) will be removed from records",
"OrganizationDeletionWarningMessage": "Organization will be deleted",
"DeletingLastOwnerWarningMessage": "An organization must have at least one owner! Therefore you cannot remove this owner",
"This{0}AlreadyExistInThisOrganization": "This {0} already exist in this organization",
"AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?",
"DeleteAll": "Delete all",

@ -29,7 +29,7 @@
"ApiKey": "API key",
"UserNameNotFound": "There is no user with username {0}",
"SuccessfullyAddedToNewsletter": "Thanks you for subscribing to our newsletter!",
"ManageProfile": "Manage your profile",
"MyProfile": "My profile",
"EmailNotValid": "Please enter a valid email address."
}
}

@ -10,7 +10,7 @@ You have different options can be used based on your requirement those will be e
## Replacing an Interface
If given service defines an interface, like the `IdentityUserAppService` class implements the `IIdentityAppService`, you can re-implement the same interface and replace the current implementation by your class. Example:
If given service defines an interface, like the `IdentityUserAppService` class implements the `IIdentityUserAppService`, you can re-implement the same interface and replace the current implementation by your class. Example:
````csharp
public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency

@ -4,7 +4,7 @@ ABP Multi-tenancy module provides base functionality to create multi tenant appl
Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) multi-tenancy as like that:
> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of a software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants.
> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants.
### Volo.Abp.MultiTenancy Package

@ -40,3 +40,7 @@ ngOnInit() {
Navigate to `/setting-management` route to see the changes:
![Custom Settings Tab](./images/custom-settings.png)
## What's Next?
- [TrackByService](./Track-By-Service.md)

@ -0,0 +1,209 @@
# How to Make HTTP Requests
## About HttpClient
Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project.
However, `HttpClient` leaves error handling to the caller (method). In other words, HTTP errors are handled manually and by hooking into the observer of the `Observable` returned.
```js
getConfig() {
this.http.get(this.configUrl).subscribe(
config => this.updateConfig(config),
error => {
// Handle error here
},
);
}
```
Although clear and flexible, handling errors this way is repetitive work, even when error processing is delegated to the store or any other injectable.
An `HttpInterceptor` is able to catch `HttpErrorResponse`  and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details.
## RestService
ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult.
### Getting Started with RestService
In order to use the `RestService`, you must inject it in your class as a dependency.
```js
import { RestService } from '@abp/ng.core';
@Injectable({
/* class metadata here */
})
class DemoService {
constructor(private rest: RestService) {}
}
```
You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**.
### How to Make a Request with RestService
You can use the `request` method of the `RestService` is for HTTP requests. Here is an example:
```js
getFoo(id: number) {
const request: Rest.Request<null> = {
method: 'GET',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, FooResponse>(request);
}
```
The `request` method always returns an `Observable<T>`. Therefore you can do the following wherever you use `getFoo` method:
```js
doSomethingWithFoo(id: number) {
this.demoService.getFoo(id).subscribe(
foo => {
// Do something with foo.
}
)
}
```
**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error.
As you see, `request` method gets a request options object with `Rest.Request<T>` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one:
```js
postFoo(body: Foo) {
const request: Rest.Request<Foo> = {
method: 'POST',
url: '/api/some/path/to/foo',
body
};
return this.rest.request<Foo, FooResponse>(request);
}
```
You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request<T>` type, which has only a few chages compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular.
### How to Disable Default Error Handler of RestService
The `request` method, used with defaults, always handles errors. Let's see how you can change that behavior and handle errors yourself:
```js
deleteFoo(id: number) {
const request: Rest.Request<null> = {
method: 'DELETE',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, void>(request, { skipHandleError: true });
}
```
`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription.
```js
removeFooFromList(id: number) {
this.demoService.deleteFoo(id).subscribe(
foo => {
// Do something with foo.
},
error => {
// Do something with error.
}
)
}
```
### How to Get a Specific API Endpoint From Application Config
Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration.
```js
putFoo(body: Foo, id: string) {
const request: Rest.Request<Foo> = {
method: 'PUT',
url: '/' + id,
body
};
return this.rest.request<Foo, void>(request, {apiName: 'foo'});
}
```
`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows:
```js
// environment.ts
export const environment = {
apis: {
default: {
url: 'https://localhost:44305',
},
foo: {
url: 'https://localhost:44305/api/some/path/to/foo',
},
},
/* rest of the environment variables here */
}
```
### How to Observe Response Object or HTTP Events Instead of Body
`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object.
```js
getSomeCustomHeaderValue() {
const request: Rest.Request<null> = {
method: 'GET',
url: '/api/some/path/that/sends/some-custom-header',
};
return this.rest.request<null, HttpResponse<any>>(
request,
{observe: Rest.Observe.Response},
).pipe(
map(response => response.headers.get('Some-Custom-Header'))
);
}
```
You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10).
## What's Next?
* [Localization](./Localization.md)

@ -133,4 +133,8 @@ Localization resources are stored in the `localization` property of `ConfigState
## See Also
* [Localization in ASP.NET Core](../../Localization.md)
* [Localization in ASP.NET Core](../../Localization.md)
## What's Next?
* [Permission Management](./Permission-Management.md)

@ -0,0 +1,114 @@
# Easy TrackByFunction Implementation
`TrackByService` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose.
## Getting Started
You do not have to provide the `TrackByService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components. For better type support, you may pass in the type of the iterated item to it.
```js
import { TrackByService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
list: Item[];
constructor(public readonly track: TrackByService<Item>) {}
}
```
> Noticed `track` is `public` and `readonly`? That is because we will see some examples where methods of `TrackByService` instance are directly called in the component's template. That may be considered as an anti-pattern, but it has its own advantage, especially when component inheritance is leveraged. You can always use public component properties instead.
**The members are also exported as separate functions.** If you do not want to inject `TrackByService`, you can always import and use those functions directly in your classes.
## Usage
There are two approaches available.
1. You may inject `TrackByService` to your component and use its members.
2. You may use exported higher-order functions directly on component properties.
### How to Track Items by a Key
You can use `by` to get a `TrackByFunction` that tracks the iterated object based on one of its keys. For type support, you may pass in the type of the iterated item to it.
```html
<!-- template of DemoComponent -->
<div *ngFor="let item of list; trackBy: track.by('id')">{%{{{ item.name }}}%}</div>
```
`by` is exported as a stand-alone function and is named `trackBy`.
```js
import { trackBy } from "@abp/ng.core";
@Component({
template: `
<div
*ngFor="let item of list; trackBy: trackById"
>
{%{{{ item.name }}}%}
</div>
`,
})
class DemoComponent {
list: Item[];
trackById = trackBy<Item>('id');
}
```
### How to Track by a Deeply Nested Key
You can use `byDeep` to get a `TrackByFunction` that tracks the iterated object based on a deeply nested key. For type support, you may pass in the type of the iterated item to it.
```html
<!-- template of DemoComponent -->
<div
*ngFor="let item of list; trackBy: track.byDeep('tenant', 'account', 'id')"
>
{%{{{ item.tenant.name }}}%}
</div>
```
`byDeep` is exported as a stand-alone function and is named `trackByDeep`.
```js
import { trackByDeep } from "@abp/ng.core";
@Component({
template: `
<div
*ngFor="let item of list; trackBy: trackByTenantAccountId"
>
{%{{{ item.name }}}%}
</div>
`,
})
class DemoComponent {
list: Item[];
trackByTenantAccountId = trackByDeep<Item>('tenant', 'account', 'id');
}
```

@ -0,0 +1,3 @@
# ABP Datatables.Net Integration for ASP.NET Core UI
TODO

@ -0,0 +1,92 @@
# Collapse
## Introduction
`abp-collapse-body` is the main container for showing and hiding content. `abp-collapse-id` is used to show and hide the content container. Can be triggered with both `abp-button` and `a` tags.
Basic usage:
````xml
<abp-button button-type="Primary" abp-collapse-id="collapseExample" text="Button with data-target" />
<a abp-button="Primary" abp-collapse-id="collapseExample"> Link with href </a>
<abp-collapse-body id="collapseExample">
Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
</abp-collapse-body>
````
## Demo
See the [collapse demo page](https://bootstrap-taghelpers.abp.io/Components/Collapse) to see it in action.
## Attributes
### show
A value indicates if the collapse body will be initialized visible or hidden. Should be one of the following values:
* `false` (default value)
* `true`
### multi
A value indicates if an `abp-collapse-body` can be shown or hidden by an element that can show/hide multiple collapse bodies. Basically, this attribute adds "multi-collapse" class to `abp-collapse-body`. Should be one of the following values:
* `false` (default value)
* `true`
Sample:
````xml
<a abp-button="Primary" abp-collapse-id="FirstCollapseExample"> Toggle first element </a>
<abp-button button-type="Primary" abp-collapse-id="SecondCollapseExample" text="Toggle second element" />
<abp-button button-type="Primary" abp-collapse-id="FirstCollapseExample SecondCollapseExample" text="Toggle both elements" />
<abp-row class="mt-3">
<abp-column size-sm="_6">
<abp-collapse-body id="FirstCollapseExample" multi="true">
Curabitur porta porttitor libero eu luctus. Praesent ultrices mattis commodo. Integer sodales massa risus, in molestie enim sagittis blandit
</abp-collapse-body>
</abp-column>
<abp-column size-sm="_6">
<abp-collapse-body id="SecondCollapseExample" multi="true">
Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et.
</abp-collapse-body>
</abp-column>
</abp-row>
````
## Accordion example
`abp-accordion` is the main container for the accordion items.
Basic usage:
````xml
<abp-accordion>
<abp-accordion-item title="Collapsible Group Item #1">
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry rtat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
</abp-accordion-item>
<abp-accordion-item title="Collapsible Group Item #2">
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
</abp-accordion-item>
<abp-accordion-item title="Collapsible Group Item #3">
Anim pariatur wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
</abp-accordion-item>
</abp-accordion>
````
## Attributes
### active
A value indicates if the accordion item will be initialized visible or hidden. Should be one of the following values:
* `false` (default value)
* `true`
### title
A value indicates the visible title of the accordion item. Should be a string value.

@ -0,0 +1,97 @@
# Dropdowns
## Introduction
`abp-dropdown` is the main container for dropdown content.
Basic usage:
````xml
<abp-dropdown>
<abp-dropdown-button text="Dropdown button" />
<abp-dropdown-menu>
<abp-dropdown-item href="#">Action</abp-dropdown-item>
<abp-dropdown-item href="#">Another action</abp-dropdown-item>
<abp-dropdown-item href="#">Something else here</abp-dropdown-item>
</abp-dropdown-menu>
</abp-dropdown>
````
## Demo
See the [dropdown demo page](https://bootstrap-taghelpers.abp.io/Components/Dropdowns) to see it in action.
## Attributes
### direction
A value indicates which direction the dropdown buttons will be displayed to. Should be one of the following values:
* `Down` (default value)
* `Up`
* `Right`
* `Left`
### dropdown-style
A value indicates if an `abp-dropdown-button` will have split icon for dropdown. Should be one of the following values:
* `Single` (default value)
* `Split`
## Menu items
`abp-dropdown-menu` is the main container for dropdown menu items.
Basic usage:
````xml
<abp-dropdown>
<abp-dropdown-button button-type="Secondary" text="Dropdown"/>
<abp-dropdown-menu>
<abp-dropdown-header>Dropdown Header</abp-dropdown-header>
<abp-dropdown-item href="#">Action</abp-dropdown-item>
<abp-dropdown-item active="true" href="#">Active action</abp-dropdown-item>
<abp-dropdown-item disabled="true" href="#">Disabled action</abp-dropdown-item>
<abp-dropdown-divider/>
<abp-dropdown-item-text>Dropdown Item Text</abp-dropdown-item-text>
<abp-dropdown-item href="#">Something else here</abp-dropdown-item>
</abp-dropdown-menu>
</abp-dropdown>
````
## Attributes
### align
A value indicates which direction `abp-dropdown-menu` items will be aligned to. Should be one of the following values:
* `Left` (default value)
* `Right`
### Additional content
`abp-dropdown-menu` can also contain additional HTML elements like headings, paragraphs, dividers or form element.
Example:
````xml
<abp-dropdown >
<abp-dropdown-button button-type="Secondary" text="Dropdown With Form"/>
<abp-dropdown-menu>
<form class="px-4 py-3">
<abp-input asp-for="EmailAddress"></abp-input>
<abp-input asp-for="Password"></abp-input>
<abp-input asp-for="RememberMe"></abp-input>
<abp-button button-type="Primary" text="Sign In" type="submit" />
</form>
<abp-dropdown-divider></abp-dropdown-divider>
<abp-dropdown-item href="#">New around here? Sign up</abp-dropdown-item>
<abp-dropdown-item href="#">Forgot password?</abp-dropdown-item>
</abp-dropdown-menu>
</abp-dropdown>
````

@ -15,6 +15,16 @@ Here, the list of components those are wrapped by the ABP Framework:
* [Buttons](Buttons.md)
* [Cards](Cards.md)
* [Alerts](Alerts.md)
* [Tabs](Tabs.md)
* [Grids](Grids.md)
* [Modals](Modals.md)
* [Collapse](Collapse.md)
* [Dropdowns](Dropdowns.md)
* [List Groups](List-Groups.md)
* [Paginator](Paginator.md)
* [Popovers](Popovers.md)
* [Progress Bars](Progress-Bars.md)
* [Tooltips](Tooltips.md)
* ...
> Until all the tag helpers are documented, you can visit https://bootstrap-taghelpers.abp.io/ to see them with live samples.

@ -0,0 +1,78 @@
# List Groups
## Introduction
`abp-list-group` is the main container for list group content.
Basic usage:
````xml
<abp-list-group>
<abp-list-group-item>Cras justo odio</abp-list-group-item>
<abp-list-group-item>Dapibus ac facilisis in</abp-list-group-item>
<abp-list-group-item>Morbi leo risus</abp-list-group-item>
<abp-list-group-item>Vestibulum at eros</abp-list-group-item>
</abp-list-group>
````
## Demo
See the [list groups demo page](https://bootstrap-taghelpers.abp.io/Components/ListGroups) to see it in action.
## Attributes
### flush
A value indicates `abp-list-group` items to remove some borders and rounded corners to render list group items edge-to-edge in a parent container. Should be one of the following values:
* `false` (default value)
* `true`
### active
A value indicates if an `abp-list-group-item` to be active. Should be one of the following values:
* `false` (default value)
* `true`
### disabled
A value indicates if an `abp-list-group-item` to be disabled. Should be one of the following values:
* `false` (default value)
* `true`
### href
A value indicates if an `abp-list-group-item` has a link. Should be a string link value.
### type
A value indicates an `abp-list-group-item` style class with a stateful background and color. Should be one of the following values:
* `Default` (default value)
* `Primary`
* `Secondary`
* `Success`
* `Danger`
* `Warning`
* `Info`
* `Light`
* `Dark`
* `Link`
### Additional content
`abp-list-group-item` can also contain additional HTML elements like spans.
Example:
````xml
<abp-list-group>
<abp-list-group-item>Cras justo odio <span abp-badge-pill="Primary">14</span></abp-list-group-item>
<abp-list-group-item>Dapibus ac facilisis in <span abp-badge-pill="Primary">2</span></abp-list-group-item>
<abp-list-group-item>Morbi leo risus <span abp-badge-pill="Primary">1</span></abp-list-group-item>
</abp-list-group>
````

@ -0,0 +1,81 @@
# Modals
## Introduction
`abp-modal` is a main element to create a modal.
Basic usage:
````xml
<abp-button button-type="Primary" data-toggle="modal" data-target="#myModal">Launch modal</abp-button>
<abp-modal centered="true" size="Large" id="myModal">
<abp-modal-header title="Modal title"></abp-modal-header>
<abp-modal-body>
Woohoo, you're reading this text in a modal!
</abp-modal-body>
<abp-modal-footer buttons="Close"></abp-modal-footer>
</abp-modal>
````
## Demo
See the [modals demo page](https://bootstrap-taghelpers.abp.io/Components/Modals) to see it in action.
## Attributes
### centered
A value indicates the positioning of the modal. Should be one of the following values:
* `false` (default value)
* `true`
### size
A value indicates the size of the modal. Should be one of the following values:
* `Default` (default value)
* `Small`
* `Large`
* `ExtraLarge`
### static
A value indicates if the modal will be static. Should be one of the following values:
* `false` (default value)
* `true`
### Additional content
`abp-modal-footer` can have multiple buttons with alignment option.
Add `@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal` to your page.
Example:
````xml
<abp-button button-type="Primary" data-toggle="modal" data-target="#myModal">Launch modal</abp-button>
<abp-modal centered="true" size="Large" id="myModal" static="true">
<abp-modal-header title="Modal title"></abp-modal-header>
<abp-modal-body>
Woohoo, you're reading this text in a modal!
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Save|AbpModalButtons.Close)" button-alignment="Between"></abp-modal-footer>
</abp-modal>
````
### button-alignment
A value indicates the positioning of your modal footer buttons. Should be one of the following values:
* `Default` (default value)
* `Start`
* `Center`
* `Around`
* `Between`
* `End`

@ -0,0 +1,57 @@
# Paginator
## Introduction
`abp-paginator` is the abp tag for pagination. Requires `Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` type of model.
Basic usage:
````xml
<abp-paginator model="Model.PagerModel" show-info="true"></abp-paginator>
````
Model:
````xml
using Microsoft.AspNetCore.Mvc.RazorPages;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
{
public class PaginatorModel : PageModel
{
public PagerModel PagerModel { get; set; }
public void OnGet(int currentPage, string sort)
{
PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort);
}
}
}
````
## Demo
See the [paginator demo page](https://bootstrap-taghelpers.abp.io/Components/Paginator) to see it in action.
## Attributes
### model
`Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` type of model can be initialized with the following data:
* `totalCount`
* `shownItemsCount`
* `currentPage`
* `pageSize`
* `pageUrl`
* `sort` (default null)
### show-info
A value indicates if an extra information about start, end and total records will be displayed. Should be one of the following values:
* `false` (default value)
* `true`

@ -0,0 +1,70 @@
# Progress Bars
## Introduction
`abp-progress-bar` is the abp tag for progress bar status.
Basic usage:
````xml
<abp-progress-bar value="70" />
<abp-progress-bar type="Warning" value="25"> %25 </abp-progress-bar>
<abp-progress-bar type="Success" value="40" strip="true"/>
<abp-progress-bar type="Dark" value="10" min-value="5" max-value="15" strip="true"> %50 </abp-progress-bar>
<abp-progress-group>
<abp-progress-part type="Success" value="25"/>
<abp-progress-part type="Danger" value="10" strip="true"> %10 </abp-progress-part>
<abp-progress-part type="Primary" value="50" animation="true" strip="true" />
</abp-progress-group>
````
## Demo
See the [progress bars demo page](https://bootstrap-taghelpers.abp.io/Components/Progress-Bars) to see it in action.
## Attributes
### value
A value indicates the current progress of the bar.
### type
A value indicates the background color of the progress bar. Should be one of the following values:
* `Default` (default value)
* `Secondary`
* `Success`
* `Danger`
* `Warning`
* `Info`
* `Light`
* `Dark`
### min-value
Minimum value of the progress bar. Default is 0.
### max-value
Maximum value of the progress bar. Default is 100.
### strip
A value indicates if the background style of the progress bar is stripped. Should be one of the following values:
* `false` (default value)
* `true`
### animation
A value indicates if the stripped background style of the progress bar is animated. Should be one of the following values:
* `false` (default value)
* `true`

@ -0,0 +1,35 @@
# Tooltips
## Introduction
`abp-tooltip` is the abp tag for tooltips.
Basic usage:
````xml
<abp-button abp-tooltip="Tooltip">
Tooltip Default
</abp-button>
<abp-button abp-tooltip-top="Tooltip">
Tooltip on top
</abp-button>
<abp-button abp-tooltip-right="Tooltip">
Tooltip on right
</abp-button>
<abp-button abp-tooltip-bottom="Tooltip">
Tooltip on bottom
</abp-button>
<abp-button disabled="true" abp-tooltip="Tooltip">
Disabled button Tooltip
</abp-button>
````
## Demo
See the [tooltips demo page](https://bootstrap-taghelpers.abp.io/Components/Tooltips) to see it in action.

@ -302,12 +302,20 @@
{
"text": "Theming",
"path": "UI/AspNetCore/Theming.md"
},
{
"text": "Customize/Extend the UI",
"path": "UI/AspNetCore/Customization-User-Interface.md"
}
]
},
{
"text": "Angular",
"items": [
{
"text": "HTTP Requests",
"path": "UI/Angular/HTTP-Requests.md"
},
{
"text": "Localization",
"path": "UI/Angular/Localization.md"
@ -327,6 +335,10 @@
{
"text": "Custom Setting Page",
"path": "UI/Angular/Custom-Setting-Page.md"
},
{
"text": "TrackByService",
"path": "UI/Angular/Track-By-Service.md"
}
]
}

@ -22,7 +22,7 @@ dotnet tool update -g Volo.Abp.Cli
生成基于ABP[启动模板](Startup-Templates/Index.md)的新解决方案.
基本用法:
用法:
````bash
abp new <解决方案名称> [options]
@ -70,7 +70,7 @@ abp new Acme.BookStore
> 需要注意的是添加的模块可能需要额外的配置,通常会在包的文档中指出.
基本用法:
用法:
````bash
abp add-package <包名> [options]
@ -94,7 +94,7 @@ abp add-package Volo.Abp.MongoDB
> 由于分层,不同的数据库提供程序选项或其他原因,业务模块通常由多个包组成. 使用`add-module`命令可以大大简化向模块添加模块的过程. 但是每个模块可能需要一些其他配置,这些配置通常在相关模块的文档中指出.
基本用法:
用法:
````bash
abp add-module <模块名称> [options]
@ -174,26 +174,26 @@ abp logout
### generate-proxy
生成typescript服务和DTO代理
为你的HTTP API生成客户端代码,简化客户端使用服务的成本. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行.
基本用法:
用法:
````bash
abp generate-proxy [options]
abp generate-proxy [options]
````
#### Options
* `--apiUrl` 或者 `-a`指定HTTP API的根URL. 如果未指定这个选项,默认使用你Angular应用程序的`environment.ts`文件API URL. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行.
* `--ui` 或者 `-u`: 指定UI框架,默认框架是angular.当前只有angular一个选项, 但我们会通过更改CLI增加新的选项. 尽请关注!
* `--module` 或者 `-m`:指定模块名. 默认模块名称为app. 如果你想所有模块,你可以指定 `--module all` 命令.
示例:
````bash
abp generate-proxy --apiUrl https://localhost:44305 --ui angular --module all
````
#### Options
* `--apiUrl` 或者 `-a`:如果未指定这个选项,默认使用你的environment.ts文件的API URL, 你可以随时使用这个选项指定API源.
* `--ui` 或者 `-u`: 指定UI框架,默认框架是angular.当前只有angular一个选项, 但我们会通过更改CLI增加新的选项. 尽请关注!
* `--module` 或者 `-m`:指定模块名. 默认模块名称为app. 如果你想所有模块,你可以指定 `--module all` 命令.
### help
CLI的基本用法信息.

@ -1,3 +1,166 @@
# 自定义应用模块: 覆盖服务
# 自定义应用模块: 重写服务
TODO...
你可能想要**更改**依赖模块的**行为(业务逻辑)**. 在这种情况下,你可以使用[依赖注入](Dependency-Injection.md)的能力替换服务,控制器甚至页面模型到你自己的实现.
注册到依赖注入的任何类,包括ABP框架的服务都可以被**替换**.
你可以根据自己的需求使用不同的选项,下面的章节中将介绍这些选项.
> 请注意,某些服务方法可能不是virtual,你可能无法override,我们会通过设计将其virtual,如果你发现任何方法不可以被覆盖,请[创建一个issue](https://github.com/abpframework/abp/issues/new)或者你直接修改后并发送**pull request**到GitHub.
## 替换接口
如果给定的服务定义了接口,像 `IdentityUserAppService` 类实现了 `IIdentityUserAppService` 接口,你可以为这个接口创建自己的实现并且替换当前的实现. 例如:
````csharp
public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency
{
//...
}
````
`MyIdentityUserAppService` 通过命名约定替换了 `IIdentityUserAppService` 的当前实现. 如果你的类名不匹配,你需要手动公开服务接口:
````csharp
[ExposeServices(typeof(IIdentityUserAppService))]
public class TestAppService : IIdentityUserAppService, ITransientDependency
{
//...
}
````
依赖注入系统允许为一个接口注册多个服务. 注入接口时会解析最后一个注入的服务. 显式的替换服务是一个好习惯.
示例:
````csharp
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService))]
public class TestAppService : IIdentityUserAppService, ITransientDependency
{
//...
}
````
使用这种方法, `IIdentityUserAppService` 接口将只会有一个实现. 也可以使用以下方法替换服务:
````csharp
context.Services.Replace(
ServiceDescriptor.Transient<IIdentityUserAppService, MyIdentityUserAppService>()
);
````
你可以在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法编写替换服务代码.
## 重写一个服务类
大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法。
### 示例: 重写服务方法
````csharp
[Dependency(ReplaceServices = true)]
public class MyIdentityUserAppService : IdentityUserAppService
{
//...
public MyIdentityUserAppService(
IdentityUserManager userManager,
IIdentityUserRepository userRepository,
IGuidGenerator guidGenerator
) : base(
userManager,
userRepository,
guidGenerator)
{
}
public override async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input)
{
if (input.PhoneNumber.IsNullOrWhiteSpace())
{
throw new AbpValidationException(
"Phone number is required for new users!",
new List<ValidationResult>
{
new ValidationResult(
"Phone number can not be empty!",
new []{"PhoneNumber"}
)
}
); }
return await base.CreateAsync(input);
}
}
````
示例中**重写**了 `IdentityUserAppService` [应用程序](Application-Services.md) `CreateAsync` 方法检查手机号码. 然后调用了基类方法继续**基本业务逻辑**. 通过这种方法你可以在基本业务逻辑**之前**和**之后**执行其他业务逻辑.
你也可以完全**重写**整个业务逻辑去创建用户,而不是调用基类方法.
### 示例: 重写领域服务
````csharp
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IdentityUserManager))]
public class MyIdentityUserManager : IdentityUserManager
{
public MyIdentityUserManager(
IdentityUserStore store,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<IdentityUserManager> logger,
ICancellationTokenProvider cancellationTokenProvider
) : base(
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger,
cancellationTokenProvider)
{
}
public override async Task<IdentityResult> CreateAsync(IdentityUser user)
{
if (user.PhoneNumber.IsNullOrWhiteSpace())
{
throw new AbpValidationException(
"Phone number is required for new users!",
new List<ValidationResult>
{
new ValidationResult(
"Phone number can not be empty!",
new []{"PhoneNumber"}
)
}
);
}
return await base.CreateAsync(user);
}
}
````
示例中类继承了 `IdentityUserManager` [领域服务](Domain-Services.md),并且重写了 `CreateAsync` 方法进行了与之前相同的手机号码检查. 结果也是一样的,但是这次我们在领域服务实现了它,假设这是我们系统的**核心领域逻辑**.
> 这里需要 `[ExposeServices(typeof(IdentityUserManager))]` attribute,因为 `IdentityUserManager` 没有定义接口 (像 `IIdentityUserManager`) ,依赖注入系统并不会按照约定公开继承类的服务(如已实现的接口).
参阅[本地化系统](Localization.md)了解如何自定义错误消息.
### 重写其他服务
控制器,框架服务,视图组件类以及其他类型注册到依赖注入的类都可以像上面的示例那样被重写.
## 如何找到服务?
[模块文档](Modules/Index.md) 包含了定义的主要服务列表. 另外 你也可以查看[源码](https://github.com/abpframework/abp/tree/dev/modules)找到所有的服务.

@ -67,7 +67,8 @@ namespace Volo.Abp.Cli.Commands
var data = JObject.Parse(json);
Logger.LogInformation("Modules are combining");
var moduleList = GetCombinedModules(data, module);
var apiNameList = new Dictionary<string, string>();
var moduleList = GetCombinedModules(data, module, out apiNameList);
if (moduleList.Count < 1)
{
@ -85,6 +86,7 @@ namespace Volo.Abp.Cli.Commands
var moduleValue = JObject.Parse(moduleItem.Value);
var rootPath = moduleItem.Key;
var apiName = apiNameList.Where(p => p.Key == rootPath).Select(p => p.Value).FirstOrDefault();
Logger.LogInformation($"{rootPath} directory is creating");
@ -105,6 +107,8 @@ namespace Volo.Abp.Cli.Commands
serviceFileText.AppendLine("");
serviceFileText.AppendLine("@Injectable({providedIn: 'root'})");
serviceFileText.AppendLine("export class [controllerName]Service {");
serviceFileText.AppendLine(" apiName = '"+ apiName + "';");
serviceFileText.AppendLine("");
serviceFileText.AppendLine(" constructor(private restService: RestService) {}");
serviceFileText.AppendLine("");
@ -309,8 +313,8 @@ namespace Volo.Abp.Cli.Commands
serviceFileText.AppendLine(
url.Contains("${")
? $" return this.restService.request({{ url: `/{url}`, method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }});"
: $" return this.restService.request({{ url: '/{url}', method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }});");
? $" return this.restService.request({{ url: `/{url}`, method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }},{{ apiName: this.apiName }});"
: $" return this.restService.request({{ url: '/{url}', method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }},{{ apiName: this.apiName }});");
serviceFileText.AppendLine(" }");
@ -369,13 +373,15 @@ namespace Volo.Abp.Cli.Commands
Logger.LogInformation("Completed!");
}
private Dictionary<string, string> GetCombinedModules(JToken data, string module)
private Dictionary<string, string> GetCombinedModules(JToken data, string module, out Dictionary<string, string> apiNameList)
{
var moduleList = new Dictionary<string, string>();
apiNameList = new Dictionary<string, string>();
foreach (var moduleItem in data["modules"])
{
var rootPath = ((string)moduleItem.First["rootPath"]).ToLower();
var apiName = (string)moduleItem.First["remoteServiceName"];
if (moduleList.Any(p => p.Key == rootPath))
{
@ -385,6 +391,7 @@ namespace Volo.Abp.Cli.Commands
}
else
{
apiNameList.Add(rootPath, apiName);
moduleList.Add(rootPath, moduleItem.First["controllers"].ToString());
}
}

@ -9,7 +9,7 @@ namespace Volo.Abp.Cli.ProjectModification
{
public ILogger<NpmGlobalPackagesChecker> Logger { get; set; }
public NpmGlobalPackagesChecker(PackageJsonFileFinder packageJsonFileFinder)
public NpmGlobalPackagesChecker()
{
Logger = NullLogger<NpmGlobalPackagesChecker>.Instance;
}

@ -25,7 +25,7 @@
<h2>Popovers</h2>
<p>Based on <a href="https://getbootstrap.com/docs/4.1/components/Popovers/" target="_blank"> Bootstrap Popovers</a>.</p>
<p>Based on <a href="https://getbootstrap.com/docs/4.1/components/popovers/" target="_blank"> Bootstrap Popovers</a>.</p>
<h4>Example</h4>

@ -0,0 +1,7 @@
namespace Volo.Blogging
{
public static class BloggingRemoteServiceConsts
{
public const string RemoteServiceName = "Blogging";
}
}

@ -9,11 +9,10 @@ namespace Volo.Blogging
typeof(AbpHttpClientModule))]
public class BloggingHttpApiClientModule : AbpModule
{
public const string RemoteServiceName = "Blogging";
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(typeof(BloggingApplicationContractsModule).Assembly, RemoteServiceName);
context.Services.AddHttpClientProxies(typeof(BloggingApplicationContractsModule).Assembly,
BloggingRemoteServiceConsts.RemoteServiceName);
}
}

@ -10,7 +10,7 @@ using Volo.Blogging.Files;
namespace Volo.Blogging
{
[RemoteService]
[RemoteService(Name = BloggingRemoteServiceConsts.RemoteServiceName)]
[Area("blogging")]
[Route("api/blogging/files")]
public class BlogFilesController : AbpController, IFileAppService

@ -9,7 +9,7 @@ using Volo.Blogging.Blogs.Dtos;
namespace Volo.Blogging
{
[RemoteService]
[RemoteService(Name = BloggingRemoteServiceConsts.RemoteServiceName)]
[Area("blogging")]
[Route("api/blogging/blogs")]
public class BlogsController : AbpController, IBlogAppService

@ -9,7 +9,7 @@ using Volo.Blogging.Comments.Dtos;
namespace Volo.Blogging
{
[RemoteService]
[RemoteService(Name = BloggingRemoteServiceConsts.RemoteServiceName)]
[Area("blogging")]
[Route("api/blogging/comments")]
public class CommentsController : AbpController, ICommentAppService

@ -8,7 +8,7 @@ using Volo.Blogging.Posts;
namespace Volo.Blogging
{
[RemoteService]
[RemoteService(Name = BloggingRemoteServiceConsts.RemoteServiceName)]
[Area("blogging")]
[Route("api/blogging/posts")]
public class PostsController : AbpController, IPostAppService

@ -9,7 +9,7 @@ using Volo.Blogging.Tagging.Dtos;
namespace Volo.Blogging
{
[RemoteService]
[RemoteService(Name = BloggingRemoteServiceConsts.RemoteServiceName)]
[Area("blogging")]
[Route("api/blogging/tags")]
public class TagsController : AbpController, ITagAppService

@ -23,8 +23,8 @@ namespace Volo.Docs.Admin.Documents
public DocumentAdminAppService(IProjectRepository projectRepository,
IDocumentRepository documentRepository,
IDocumentSourceFactory documentStoreFactory,
IDistributedCache<DocumentUpdateInfo> documentUpdateCache,
IDocumentSourceFactory documentStoreFactory,
IDistributedCache<DocumentUpdateInfo> documentUpdateCache,
IDocumentFullSearch documentFullSearch)
{
_projectRepository = projectRepository;
@ -91,11 +91,21 @@ namespace Volo.Docs.Admin.Documents
foreach (var doc in docs)
{
var project = projects.FirstOrDefault(x => x.Id == doc.ProjectId);
if (project != null && (doc.FileName == project.NavigationDocumentName || doc.FileName == project.ParametersDocumentName))
if (project == null)
{
continue;
}
if (doc.FileName == project.NavigationDocumentName)
{
continue;
}
if (doc.FileName == project.ParametersDocumentName)
{
continue;
}
await _documentFullSearch.AddOrUpdateAsync(doc);
}
}

@ -23,7 +23,7 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
public async Task CreateIndexIfNeededAsync(CancellationToken cancellationToken = default)
{
CheckEsEnabled();
ValidateElasticSearchEnabled();
var client = _clientProvider.GetClient();
@ -51,11 +51,11 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
public async Task AddOrUpdateAsync(Document document, CancellationToken cancellationToken = default)
{
CheckEsEnabled();
ValidateElasticSearchEnabled();
var client = _clientProvider.GetClient();
var existsResponse = await client.DocumentExistsAsync<EsDocument>(DocumentPath<EsDocument>.Id(document.Id),
var existsResponse = await client.DocumentExistsAsync(DocumentPath<EsDocument>.Id(document.Id),
x => x.Index(_options.IndexName), cancellationToken);
HandleError(existsResponse);
@ -73,12 +73,12 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
if (!existsResponse.Exists)
{
HandleError(await client.IndexAsync<EsDocument>(esDocument,
HandleError(await client.IndexAsync(esDocument,
x => x.Id(document.Id).Index(_options.IndexName), cancellationToken));
}
else
{
HandleError(await client.UpdateAsync<EsDocument>(DocumentPath<EsDocument>.Id(document.Id),
HandleError(await client.UpdateAsync(DocumentPath<EsDocument>.Id(document.Id),
x => x.Doc(esDocument).Index(_options.IndexName), cancellationToken));
}
@ -86,7 +86,7 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
{
CheckEsEnabled();
ValidateElasticSearchEnabled();
HandleError(await _clientProvider.GetClient()
.DeleteAsync(DocumentPath<Document>.Id(id), x => x.Index(_options.IndexName), cancellationToken));
@ -96,7 +96,7 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
string version, int? skipCount = null, int? maxResultCount = null,
CancellationToken cancellationToken = default)
{
CheckEsEnabled();
ValidateElasticSearchEnabled();
var request = new SearchRequest
{
@ -150,7 +150,6 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
}
};
//var json = _clientProvider.GetClient().RequestResponseSerializer.SerializeToString(request);
var response = await _clientProvider.GetClient().SearchAsync<EsDocument>(request, cancellationToken);
HandleError(response);
@ -178,7 +177,7 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
}
}
protected void CheckEsEnabled()
protected void ValidateElasticSearchEnabled()
{
if (!_options.Enable)
{

@ -90,7 +90,10 @@
</label>
</div>
<select asp-items="Model.ProjectSelectItems" class="form-control" onchange="window.location.pathname = this.value"></select>
<select asp-items="Model.ProjectSelectItems"
class="form-control"
onchange="window.location.pathname = this.value">
</select>
</div>
</div>
</div>
@ -109,7 +112,10 @@
</label>
</div>
<select asp-items="Model.VersionSelectItems" class="form-control" onchange="if (this.value) { window.location.replace(this.value) }"></select>
<select asp-items="Model.VersionSelectItems"
class="form-control"
onchange="if (this.value) { window.location.replace(this.value) }">
</select>
</div>
</div>
</div>
@ -127,7 +133,10 @@
<i class="fa fa-globe" aria-hidden="true" data-toggle="tooltip" title="@L["Language"]"></i>
</label>
</div>
<select asp-items="Model.LanguageSelectListItems" class="form-control" onchange="window.location.replace(this.value)"></select>
<select asp-items="Model.LanguageSelectListItems"
class="form-control"
onchange="window.location.replace(this.value)">
</select>
</div>
</div>
</div>
@ -142,7 +151,12 @@
<label class="input-group-text"><i class="fa fa-filter"></i></label>
</div>
<input class="form-control" id="filter" type="search" data-search-url="@Model." placeholder="@L["FilterTopics"].Value" aria-label="Filter">
<input class="form-control"
id="filter"
type="search"
data-search-url="@Model."
placeholder="@L["FilterTopics"].Value"
aria-label="Filter">
</div>
</div>
</div>
@ -156,7 +170,12 @@
<label class="input-group-text"><i class="fa fa-filter"></i></label>
</div>
<input class="form-control" id="fullsearch" type="search" data-fullsearch-url="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/" placeholder="@L["FullSearch"].Value" aria-label="Filter">
<input class="form-control"
id="fullsearch"
type="search"
data-fullsearch-url="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/"
placeholder="@L["FullSearch"].Value"
aria-label="Filter">
</div>
</div>
</div>
@ -204,7 +223,9 @@
<div class="float-right">
@if (!string.IsNullOrEmpty(Model.Document.EditLink))
{
<a href="@Model.Document.EditLink" target="_blank"> <i class="fa fa-edit"></i> @(L["Edit"]) (@L["LastEditTime"]: @Model.Document.LastUpdatedTime.ToShortDateString())</a>
<a href="@Model.Document.EditLink" target="_blank">
<i class="fa fa-edit"></i> @(L["Edit"]) (@L["LastEditTime"]: @Model.Document.LastUpdatedTime.ToShortDateString())
</a>
}
</div>
@ -216,7 +237,11 @@
{
<a href="@contributor.UserProfileUrl" target="_blank">
<img src="@contributor.AvatarUrl"
class="rounded-circle" height="21" width="21" title="@contributor.Username" />
class="rounded-circle"
alt="Avatar"
height="21"
width="21"
title="@contributor.Username" />
</a>
}
}
@ -290,9 +315,7 @@
</div>
<div class="col-md-2 docs-page-index position-relative bg-light">
<div id="scroll-index" class="docs-inner-anchors mt-2">
<h5>@L["InThisDocument"]</h5>
<nav id="docs-sticky-index" class="navbar index-scroll">
</nav>
@ -302,10 +325,8 @@
<a href="javascript:;" class="scroll-top-btn"><i class="fa fa-chevron-up"></i> @L["GoToTop"]</a>
</div>
</div>
</div>
</div>
}
else
{
@ -318,7 +339,6 @@
</a>
</div>
}
</div>
</div>
}

@ -119,16 +119,17 @@
};
var setQueryString = function () {
clearQueryString();
var uri = window.location.href.toString();
var comboboxes = $(".doc-section-combobox");
if (comboboxes.length < 1) {
return;
}
var hash = document.location.hash;
clearQueryString();
var uri = window.location.href.toString();
var new_uri = uri + "?";
for (var i = 0; i < comboboxes.length; i++) {
@ -142,7 +143,7 @@
}
}
window.history.replaceState({}, document.title, new_uri);
window.history.replaceState({}, document.title, new_uri + hash);
};
var getTenYearsLater = function () {

@ -0,0 +1,7 @@
namespace Volo.Abp.FeatureManagement
{
public class FeatureManagementRemoteServiceConsts
{
public const string RemoteServiceName = "FeatureManagement";
}
}

@ -9,13 +9,11 @@ namespace Volo.Abp.FeatureManagement
typeof(AbpHttpClientModule))]
public class AbpFeatureManagementHttpApiClientModule : AbpModule
{
public const string RemoteServiceName = "AbpFeatureManagement";
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(
typeof(AbpFeatureManagementApplicationContractsModule).Assembly,
RemoteServiceName
FeatureManagementRemoteServiceConsts.RemoteServiceName
);
}
}

@ -4,7 +4,7 @@ using Volo.Abp.AspNetCore.Mvc;
namespace Volo.Abp.FeatureManagement
{
[RemoteService]
[RemoteService(Name = FeatureManagementRemoteServiceConsts.RemoteServiceName)]
[Area("abp")]
public class FeaturesController : AbpController, IFeatureAppService
{

@ -12,6 +12,18 @@ namespace Volo.Abp.IdentityServer
)]
public class AbpIdentityServerTestBaseModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpIdentityServerBuilderOptions>(options =>
{
options.AddDeveloperSigningCredential = false;
});
PreConfigure<IIdentityServerBuilder>(identityServerBuilder =>
{
identityServerBuilder.AddDeveloperSigningCredential(false, System.Guid.NewGuid().ToString());
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAlwaysAllowAuthorization();

@ -0,0 +1,7 @@
namespace Volo.Abp.PermissionManagement
{
public class PermissionManagementRemoteServiceConsts
{
public const string RemoteServiceName = "AbpPermissionManagement";
}
}

@ -9,13 +9,11 @@ namespace Volo.Abp.PermissionManagement
typeof(AbpHttpClientModule))]
public class AbpPermissionManagementHttpApiClientModule : AbpModule
{
public const string RemoteServiceName = "AbpPermissionManagement";
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(
typeof(AbpPermissionManagementApplicationContractsModule).Assembly,
RemoteServiceName
PermissionManagementRemoteServiceConsts.RemoteServiceName
);
}
}

@ -4,7 +4,7 @@ using Volo.Abp.AspNetCore.Mvc;
namespace Volo.Abp.PermissionManagement
{
[RemoteService]
[RemoteService(Name = PermissionManagementRemoteServiceConsts.RemoteServiceName)]
[Area("abp")]
public class PermissionsController : AbpController, IPermissionAppService
{

@ -0,0 +1,7 @@
namespace Volo.Abp.TenantManagement
{
public class TenantManagementRemoteServiceConsts
{
public const string RemoteServiceName = "AbpTenantManagement";
}
}

@ -15,6 +15,8 @@
"Permission:Edit": "編輯",
"Permission:Delete": "刪除",
"Permission:ManageConnectionStrings": "管理資料庫連線字串",
"Permission:ManageFeatures": "管理功能"
"Permission:ManageFeatures": "管理功能",
"DisplayName:AdminEmailAddress": "管理者信箱",
"DisplayName:AdminPassword": "管理者密碼"
}
}
}

@ -9,13 +9,11 @@ namespace Volo.Abp.TenantManagement
typeof(AbpHttpClientModule))]
public class AbpTenantManagementHttpApiClientModule : AbpModule
{
public const string RemoteServiceName = "AbpTenantManagement";
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClientProxies(
typeof(AbpTenantManagementApplicationContractsModule).Assembly,
RemoteServiceName
TenantManagementRemoteServiceConsts.RemoteServiceName
);
}
}

@ -7,7 +7,7 @@ using Volo.Abp.AspNetCore.Mvc;
namespace Volo.Abp.TenantManagement
{
[Controller]
[RemoteService]
[RemoteService(Name = TenantManagementRemoteServiceConsts.RemoteServiceName)]
[Area("multi-tenancy")]
[Route("api/multi-tenancy/tenants")]
public class TenantController : AbpController, ITenantAppService //TODO: Throws exception on validation if we inherit from Controller

@ -44,6 +44,7 @@ namespace Volo.Abp.TenantManagement.Web.Pages.TenantManagement.Tenants
public string AdminEmailAddress { get; set; }
[Required]
[DataType(DataType.Password)]
[MaxLength(128)]
public string AdminPassword { get; set; }
}

@ -6,6 +6,7 @@
"typescript.tsdk": "../node_modules/typescript/lib",
"workbench.colorCustomizations": {
"activityBar.background": "#258ecd",
"activityBar.activeBackground": "#258ecd",
"activityBar.activeBorder": "#f0aed7",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",

@ -21,19 +21,19 @@
"generate:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"devDependencies": {
"@abp/ng.account": "~2.2.0",
"@abp/ng.account.config": "~2.2.0",
"@abp/ng.core": "~2.2.0",
"@abp/ng.feature-management": "~2.2.0",
"@abp/ng.identity": "~2.2.0",
"@abp/ng.identity.config": "~2.2.0",
"@abp/ng.permission-management": "~2.2.0",
"@abp/ng.setting-management": "~2.2.0",
"@abp/ng.setting-management.config": "~2.2.0",
"@abp/ng.tenant-management": "~2.2.0",
"@abp/ng.tenant-management.config": "~2.2.0",
"@abp/ng.theme.basic": "~2.2.0",
"@abp/ng.theme.shared": "~2.2.0",
"@abp/ng.account": "~2.3.0",
"@abp/ng.account.config": "~2.3.0",
"@abp/ng.core": "^2.3.0",
"@abp/ng.feature-management": "^2.3.0",
"@abp/ng.identity": "~2.3.0",
"@abp/ng.identity.config": "~2.3.0",
"@abp/ng.permission-management": "^2.3.0",
"@abp/ng.setting-management": "~2.3.0",
"@abp/ng.setting-management.config": "~2.3.0",
"@abp/ng.tenant-management": "~2.3.0",
"@abp/ng.tenant-management.config": "~2.3.0",
"@abp/ng.theme.basic": "~2.3.0",
"@abp/ng.theme.shared": "^2.3.0",
"@angular-builders/jest": "^8.2.0",
"@angular-devkit/build-angular": "~0.803.21",
"@angular-devkit/build-ng-packagr": "~0.803.21",
@ -83,6 +83,7 @@
"snq": "^1.0.3",
"symlink-manager": "^1.4.2",
"ts-node": "~7.0.0",
"ts-toolbelt": "^6.3.6",
"tsickle": "^0.37.0",
"tslint": "~5.20.0",
"typescript": "~3.5.3",

@ -2,10 +2,11 @@ import { HttpClient, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { Observable, throwError } from 'rxjs';
import { catchError, take, tap } from 'rxjs/operators';
import { catchError } from 'rxjs/operators';
import { RestOccurError } from '../actions/rest.actions';
import { Rest } from '../models/rest';
import { ConfigState } from '../states/config.state';
import { isUndefinedOrEmptyString } from '../utils/common-utils';
@Injectable({
providedIn: 'root',
@ -13,47 +14,40 @@ import { ConfigState } from '../states/config.state';
export class RestService {
constructor(private http: HttpClient, private store: Store) {}
private getApiFromStore(apiName: string): string {
return this.store.selectSnapshot(ConfigState.getApiUrl(apiName));
}
handleError(err: any): Observable<any> {
this.store.dispatch(new RestOccurError(err));
console.error(err);
return throwError(err);
}
// TODO: Deprecate service or improve interface in v3.0
request<T, R>(
request: HttpRequest<T> | Rest.Request<T>,
config?: Rest.Config,
api?: string,
): Observable<R> {
config = config || ({} as Rest.Config);
const { observe = Rest.Observe.Body, skipHandleError } = config;
const url =
(api || this.store.selectSnapshot(ConfigState.getApiUrl(config.apiName))) + request.url;
api = api || this.getApiFromStore(config.apiName);
const { method, params, ...options } = request;
const { observe = Rest.Observe.Body, skipHandleError } = config;
return this.http
.request<T>(method, url, {
.request<R>(method, api + request.url, {
observe,
...(params && {
params: Object.keys(params).reduce(
(acc, key) => ({
...acc,
...(typeof params[key] !== 'undefined' &&
params[key] !== '' && { [key]: params[key] }),
}),
{},
),
params: Object.keys(params).reduce((acc, key) => {
const value = params[key];
if (!isUndefinedOrEmptyString(value)) acc[key] = value;
return acc;
}, {}),
}),
...options,
} as any)
.pipe(
observe === Rest.Observe.Body ? take(1) : tap(),
catchError(err => {
if (skipHandleError) {
return throwError(err);
}
return this.handleError(err);
}),
);
.pipe(catchError(err => (skipHandleError ? throwError(err) : this.handleError(err))));
}
}

@ -1,18 +1,17 @@
import { Injectable, TrackByFunction } from '@angular/core';
import { O } from 'ts-toolbelt';
export const trackBy = <T = any>(key: keyof T): TrackByFunction<T> => (_, item) => item[key];
export const trackByDeep = <T = any>(
...keys: T extends object ? O.Paths<T> : never
): TrackByFunction<T> => (_, item) => keys.reduce((acc, key) => acc[key], item);
@Injectable({
providedIn: 'root',
})
export class TrackByService<ItemType = any> {
by<T = ItemType>(key: keyof T): TrackByFunction<T> {
return ({}, item) => item[key];
}
byDeep<T = ItemType>(...keys: (string | number)[]): TrackByFunction<T> {
return ({}, item) => keys.reduce((acc, key) => acc[key], item);
}
by = trackBy;
bySelf<T = ItemType>(): TrackByFunction<T> {
return ({}, item) => item;
}
byDeep = trackByDeep;
}

@ -262,7 +262,8 @@ export class ConfigState {
route.url = `/${route.path}`;
}
route.order = route.order || route.order === 0 ? route.order : parent.children.length;
route.children = route.children || [];
route.order = route.order || route.order === 0 ? route.order : (parent.children || []).length;
parent.children = [...(parent.children || []), route].sort((a, b) => a.order - b.order);
flattedRoutes[index] = parent;

@ -1,4 +1,4 @@
import { noop } from '../utils';
import { isUndefinedOrEmptyString, noop } from '../utils';
describe('CommonUtils', () => {
describe('#noop', () => {
@ -7,4 +7,20 @@ describe('CommonUtils', () => {
expect(noop()()).toBeUndefined();
});
});
describe('#isUndefinedOrEmptyString', () => {
test.each`
value | expected
${null} | ${false}
${0} | ${false}
${true} | ${false}
${'x'} | ${false}
${{}} | ${false}
${[]} | ${false}
${undefined} | ${true}
${''} | ${true}
`('should return $expected when given parameter is $value', ({ value, expected }) => {
expect(isUndefinedOrEmptyString(value)).toBe(expected);
});
});
});

@ -375,6 +375,7 @@ describe('ConfigState', () => {
describe('#AddRoute', () => {
const newRoute = {
name: 'My new page',
children: [],
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,

@ -1,9 +1,8 @@
import { ConfigState } from '@abp/ng.core';
import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/jest';
import { NgxsModule, Store } from '@ngxs/store';
import { interval, of, Subscription, throwError, timer } from 'rxjs';
import { of, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { RestOccurError } from '../actions';
import { Rest } from '../models';
import { RestService } from '../services/rest.service';
@ -36,6 +35,10 @@ describe('HttpClient testing', () => {
});
});
afterEach(() => {
spectator.controller.verify();
});
test('should send a GET request with params', () => {
spectator.service
.request({ method: HttpMethod.GET, url: '/test', params: { id: 1 } })
@ -66,34 +69,16 @@ describe('HttpClient testing', () => {
spectator.expectOne('bar' + '/test', HttpMethod.GET);
});
test('should close the subscriber when observe equal to body', done => {
jest.spyOn(spectator.httpClient, 'request').mockReturnValue(interval(50));
test('should complete upon successful request', done => {
const complete = jest.fn(done);
const subscriber: Subscription = spectator.service
.request({ method: HttpMethod.GET, url: '/test' }, { observe: Rest.Observe.Body })
.subscribe();
spectator.service.request({ method: HttpMethod.GET, url: '/test' }).subscribe({ complete });
timer(51).subscribe(() => {
expect(subscriber.closed).toBe(true);
done();
});
});
test('should open the subscriber when observe not equal to body', done => {
jest.spyOn(spectator.httpClient, 'request').mockReturnValue(interval(50));
const subscriber: Subscription = spectator.service
.request({ method: HttpMethod.GET, url: '/test' }, { observe: Rest.Observe.Events })
.subscribe();
timer(51).subscribe(() => {
expect(subscriber.closed).toBe(false);
done();
});
const req = spectator.expectOne(api + '/test', HttpMethod.GET);
spectator.flushAll([req], [{}]);
});
test('should handle the error', () => {
jest.spyOn(spectator.httpClient, 'request').mockReturnValue(throwError('Testing error'));
const spy = jest.spyOn(store, 'dispatch');
spectator.service
@ -101,15 +86,17 @@ describe('HttpClient testing', () => {
.pipe(
catchError(err => {
expect(err).toBeTruthy();
expect(spy.mock.calls[0][0] instanceof RestOccurError).toBe(true);
expect(spy).toHaveBeenCalled();
return of(null);
}),
)
.subscribe();
const req = spectator.expectOne(api + '/test', HttpMethod.GET);
spectator.flushAll([req], [throwError('Testing error')]);
});
test('should not handle the error when skipHandleError is true', () => {
jest.spyOn(spectator.httpClient, 'request').mockReturnValue(throwError('Testing error'));
const spy = jest.spyOn(store, 'dispatch');
spectator.service
@ -120,10 +107,13 @@ describe('HttpClient testing', () => {
.pipe(
catchError(err => {
expect(err).toBeTruthy();
expect(spy.mock.calls).toHaveLength(0);
expect(spy).toHaveBeenCalledTimes(0);
return of(null);
}),
)
.subscribe();
const req = spectator.expectOne(api + '/test', HttpMethod.GET);
spectator.flushAll([req], [throwError('Testing error')]);
});
});

@ -11,23 +11,11 @@ describe('TrackByService', () => {
describe('#byDeep', () => {
it('should return a function which tracks a deeply-nested property', () => {
expect(
service.byDeep(
'a',
'b',
'c',
1,
'x',
)(284, {
a: { b: { c: [{ x: 1035 }, { x: 1036 }, { x: 1037 }] } },
}),
).toBe(1036);
});
});
const obj = {
a: { b: { c: { x: 1036 } } },
};
describe('#bySelf', () => {
it('should return a function which tracks the item', () => {
expect(service.bySelf()(284, 'X')).toBe('X');
expect(service.byDeep<typeof obj>('a', 'b', 'c', 'x')(284, obj)).toBe(1036);
});
});
});

@ -3,3 +3,7 @@ export function noop() {
const fn = function() {};
return fn;
}
export function isUndefinedOrEmptyString(value: unknown): boolean {
return value === undefined || value === '';
}

@ -2,26 +2,26 @@
# yarn lockfile v1
"@abp/ng.account.config@^2.2.0", "@abp/ng.account.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account.config/-/ng.account.config-2.2.0.tgz#420396fc55eadd9a5c179e87075a51437ebc964c"
integrity sha512-5BpxFnXCeCDR+m3qGMKn8rSMGwBb4mkwtejVAJXCVYoMXL8x2J9uRgDf9fkdudqpls+BIgB8BX1tRyJiY+Bg8Q==
"@abp/ng.account.config@^2.3.0", "@abp/ng.account.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account.config/-/ng.account.config-2.3.0.tgz#28d65e2eb889d8b44fc726edbed7509214e8034a"
integrity sha512-Vg4+8PvGfgUC+pFtPIS53ZekJM+O4JZ8wGPGaZ/ySLpk2oSfzC/5RFS2rKq3cmySeRCJunmQinCAlBCm5zir8A==
dependencies:
tslib "^1.9.0"
"@abp/ng.account@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account/-/ng.account-2.2.0.tgz#3a200a46f83c36ae89a6724a6a2226a5f846641b"
integrity sha512-OwTOxXDI3BJ1MrlR6WtvDmNIkqi1OCWlq7MvUhuFPVmIIUI3OjxHNASk1NfWMSlu6amXDxuFEey4ItrMKnAJog==
"@abp/ng.account@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account/-/ng.account-2.3.0.tgz#9ca2a564c177c43b53f69f141405914baefbc7b2"
integrity sha512-kJCek8woGGEC1WTgo75Qr2ucyD5VV1nqqnkw8UMJ96/pqASpBJHczGTL8R4ug961i4vexKDnMbiv0SmfMlBDig==
dependencies:
"@abp/ng.account.config" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.account.config" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.core@^2.2.0", "@abp/ng.core@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-2.2.0.tgz#a553e845f7bc43838704eb15d8684869dfcb053b"
integrity sha512-HtyHJYPY6kKqySt/afgFT5j6yaN7Bx4MMvIGWHqFqZsQChWceagLk5SBFTOjCE1FgsewX2a7OT452bKjgsWsrg==
"@abp/ng.core@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-2.3.0.tgz#96bad951da07e589f11a0345171e1a142463671e"
integrity sha512-6SsgcRJQWjXvyEZ7VO2ekOoVmKkFB17vJgTvQLiyB+j2t9guAo3LPDcMGGKNum/oInkiYczf8MY7sculrElyKQ==
dependencies:
"@angular/localize" "~9.0.2"
"@ngxs/router-plugin" "^3.6.2"
@ -33,86 +33,86 @@
snq "^1.0.3"
tslib "^1.9.0"
"@abp/ng.feature-management@^2.2.0", "@abp/ng.feature-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-2.2.0.tgz#04f959ddb62a0abd99a84fb4e6ec935a796418d4"
integrity sha512-Cw0GRi+6LX5oKDwEvJJyUoh7M4hvaUE5TsuP1E3Hicg/1mMSyUDXVrLBOvAeWWolyfFTpFKLppAvoEEMpUt2eg==
"@abp/ng.feature-management@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-2.3.0.tgz#91172fa9f308b3a792a2bf1e762e24560556f412"
integrity sha512-ShytiV1SC3PwP4Hs8Ss3bk2UAZBXteHKWcrIhAvqtWsOQ2aql6e7t+FutRA3wtUuRdX6PTrMRXbMRvhCwGZUKQ==
dependencies:
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.identity.config@^2.2.0", "@abp/ng.identity.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity.config/-/ng.identity.config-2.2.0.tgz#ac050ed632624c490d8957a18606622637d1d6f3"
integrity sha512-sHRG0iRFrGtF2pNnKcjBaeupg39V8easzUFPU42/SVPi0XyAumrOZRbKkN2CBl0WYedqARTZxE5/nejRMXX4Fg==
"@abp/ng.identity.config@^2.3.0", "@abp/ng.identity.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity.config/-/ng.identity.config-2.3.0.tgz#e9f4904d60f94cc01b3254e0221d7cdb2274aa3b"
integrity sha512-bqCaPHCwaHUfAfNfFskGTJHGvv+hPK9Tmm7PouVa884AmeQs2j0Gwl3o93YHK2VwXENV09qb01feXapSVPsmTw==
dependencies:
tslib "^1.9.0"
"@abp/ng.identity@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-2.2.0.tgz#1cc7ffa4e2aae462d8ce2399f94cf5ad047fefae"
integrity sha512-G/hrMg/PaN0tA871D52AuKlUhsLWfWSblK0XqQiRmMp/ozsEYSvAV91n/pEScm7qx0RF01K0J5K5V8Cjb4LTyA==
"@abp/ng.identity@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-2.3.0.tgz#c973f12f5490b1d29c80f510540b18343f998b8d"
integrity sha512-vBLLxCax7MoHilakpW9XMRFGUXcdMM0syiz0PtkgKmYADIQIQ9AzcfwslK2D6wwy447s6NIoGnThtfbshoRtLw==
dependencies:
"@abp/ng.identity.config" "^2.2.0"
"@abp/ng.permission-management" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.identity.config" "^2.3.0"
"@abp/ng.permission-management" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.permission-management@^2.2.0", "@abp/ng.permission-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-2.2.0.tgz#56bece037aa094400f8d5585db0bfd5718437ae0"
integrity sha512-OeUZzZV+2TTWOhmpwFTvar9/4IpKz5EhI/6uabu3pOtIsU2Ms2OBbjwVUSKbhLT7e0+z5MM1nPCDoXIlYv22wA==
"@abp/ng.permission-management@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-2.3.0.tgz#16599a2e1583c9d6769edb1dc9b78a45b75b7402"
integrity sha512-vJSfcmXCXpBHMjeRb/0QoKlAcvpCQ/qS2quHFUr23nriXIeNxgEdCVHkTBZU8FMxfyTfwwrFRzF2oxn33gubbg==
dependencies:
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.setting-management.config@^2.2.0", "@abp/ng.setting-management.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management.config/-/ng.setting-management.config-2.2.0.tgz#31cf94a785fc2d5c3f1bcfe2c79afc0f1ab419ee"
integrity sha512-vZhBvKFZ6puWwujkzEEhqyQiMKdTmAeJjSxlUPRyIuwowrZhPnUk1r0ghyOLTC5fC1TDaUXF1mZ2UsBFMDvuiw==
"@abp/ng.setting-management.config@^2.3.0", "@abp/ng.setting-management.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management.config/-/ng.setting-management.config-2.3.0.tgz#80b9ebc659c34be4c4c45cc6f2fe2b593f491aab"
integrity sha512-nj6Hl8hlzrGJFJZo4d9DWQtf1PCSFnXup/3ajqMOCPPE+oBItY3aNY05jDyGgdr2wEdyWo/u9Invy3Jsvq8itQ==
dependencies:
tslib "^1.9.0"
"@abp/ng.setting-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-2.2.0.tgz#e1c976bf69bbfaa452489b8968b8669c3fba7710"
integrity sha512-LL6CUi0qpS0+9kPz/T0n1U+Kpu9gwI49+d/+xVDG+lIziGqukgwgoUwlEv+Lk0ak+RC5noLurvvJaaKd3l3mPw==
"@abp/ng.setting-management@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-2.3.0.tgz#478c603f67416df763228bc958b6ea05e7d13dd7"
integrity sha512-xJk09NdpXeg3/KezvKl+h0B4T08Hy1SofOATQpVTrE4TIsFxyXKmasMcSY4i/3+wwE1umH+6TZJwPE+pXTu9Ng==
dependencies:
"@abp/ng.setting-management.config" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.setting-management.config" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.tenant-management.config@^2.2.0", "@abp/ng.tenant-management.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management.config/-/ng.tenant-management.config-2.2.0.tgz#16d87f31ec069e1f3a8351ef3be9a05a3002ed26"
integrity sha512-lfW9lGERn9PBIRseJajQ0GSxo1+wfRxO7Ic/lSSPxhUbsmwg6afquYTcGiU0d+4QQOBY47ga3n0IVrNqWq1pmw==
"@abp/ng.tenant-management.config@^2.3.0", "@abp/ng.tenant-management.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management.config/-/ng.tenant-management.config-2.3.0.tgz#8501a3a53f1abac8a65a87782752afb2ba648bb1"
integrity sha512-gGqg7rZd5X37z9glYF2lSiFpJ3Lyi1NdqHnaxdCTui+3/weMo/5RKlf+ilUAPqR5YAMVSLi4mBYYsuShWEBBIQ==
dependencies:
tslib "^1.9.0"
"@abp/ng.tenant-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-2.2.0.tgz#0ce498eaf9f65ef0255fc23949a47f8ae36cf5c8"
integrity sha512-v9Y5F9fm2EXYteCWKI8QODN4ETmSdh6K7gC5Y3+/N+QaUAod8JxFNX0EIXzFGnSLbiZ0O1xA/TRJruQTv1m3SA==
"@abp/ng.tenant-management@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-2.3.0.tgz#5ec092ad9c597d4aa9f2f849fc12e955d2d24696"
integrity sha512-LyJaXuzgZr2tfFPknuGz1spOzfxfaEhPuQ6zHBOhVfQ6KtMBeagsQQoEiLJMtcygR0MRox0kzmUiyTKm4H5DoA==
dependencies:
"@abp/ng.feature-management" "^2.2.0"
"@abp/ng.tenant-management.config" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.feature-management" "^2.3.0"
"@abp/ng.tenant-management.config" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.theme.basic@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-2.2.0.tgz#7a086a27daa3bec16962dd62dfb2f39a1538e82f"
integrity sha512-fpdDjjhEQZtaZvFkVi5uhqZoYyrxCWJeQgGFvLS36TqYqvJVyoMeJBlVw0CgbY3F+u5V/GmZSVxvZAAJDpUYhg==
"@abp/ng.theme.basic@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-2.3.0.tgz#e5857864ae4c3274a57cc785b06cccd0ff3515a8"
integrity sha512-LuAlKqmqEFUUwI/ruB6aO1rhfsCD19Pt7PCE3M+vr/KvlYAKb89K1U8nphIfMV8PCz9IU07BwvTn/T2qIt39HQ==
dependencies:
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.theme.shared@^2.2.0", "@abp/ng.theme.shared@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-2.2.0.tgz#438f77498df3e2f25a1ecf9adb77a9ee5a71d2c5"
integrity sha512-w7TnDbdHpOFcT12wt/9nZDH9PkyZdTP6W+tJIGeH6zOgWC8V4MDX8Ulc9e/ZvQ8u0qRLOEmk3aE5odFQUHSlJw==
"@abp/ng.theme.shared@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-2.3.0.tgz#5b13b8e170fb0c2a4afca34434bd455ee8dfb9d6"
integrity sha512-keYnD17K8QkdSLqBbsATfeh7KwKNoUj/XsHZO8hawE3OfRuy4qYY9xghGFZUUqdIE5kF6gKlLKj4ZTZmuCvOmQ==
dependencies:
"@abp/ng.core" "^2.2.0"
"@abp/ng.core" "^2.3.0"
"@fortawesome/fontawesome-free" "^5.12.1"
"@ng-bootstrap/ng-bootstrap" "^5.3.0"
"@ngx-validate/core" "^0.0.7"
@ -11685,6 +11685,11 @@ ts-node@~7.0.0:
source-map-support "^0.5.6"
yn "^2.0.0"
ts-toolbelt@^6.3.6:
version "6.3.6"
resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.3.6.tgz#2bde29106c013ed520c32f30e1248daf8fd4f5f9"
integrity sha512-eVzym+LyQodOCfyVyQDQ6FGYbO2Xf9Nc4dGLRKlKSUpAs+8qQWHG+grDiA3ciEuNPNZ0qJnNIYkdqBW1rCWuUA==
tsickle@^0.37.0:
version "0.37.1"
resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.37.1.tgz#2f8a87c1b15766e866457bd06fb6c0e0d84eed09"

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -38,15 +39,14 @@ namespace MyCompanyName.MyProjectName.Data
await MigrateHostDatabaseAsync();
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var i = 0;
var tenants = await _tenantRepository.GetListAsync();
foreach (var tenant in tenants)
foreach (var tenant in tenants.Where(t => t.ConnectionStrings.Any()))
{
i++;
using (_currentTenant.Change(tenant.Id))
{
Logger.LogInformation($"Migrating {tenant.Name} database schema... ({i} of {tenants.Count})");
Logger.LogInformation($"Migrating {tenant.Name} database schema... ({++i} of {tenants.Count})");
await MigrateTenantDatabasesAsync(tenant);
Logger.LogInformation($"Successfully completed {tenant.Name} database migrations.");
}

Loading…
Cancel
Save