mirror of https://github.com/abpframework/abp.git
109 changed files with 2312 additions and 511 deletions
@ -0,0 +1,3 @@ |
|||
# BLOB Storing Azure Provider |
|||
|
|||
This feature will be available with v3.0! |
|||
@ -0,0 +1,177 @@ |
|||
# BLOB Storing: Creating a Custom Provider |
|||
|
|||
This document explains how you can create a new storage provider for the BLOB storing system with an example. |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to create a new storage provider. |
|||
|
|||
## Example Implementation |
|||
|
|||
The first step is to create a class implements the `IBlobProvider` interface or inherit from the `BlobProviderBase` abstract class. |
|||
|
|||
````csharp |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency |
|||
{ |
|||
public override Task SaveAsync(BlobProviderSaveArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
|
|||
public override Task<bool> DeleteAsync(BlobProviderDeleteArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
|
|||
public override Task<bool> ExistsAsync(BlobProviderExistsArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
|
|||
public override Task<Stream> GetOrNullAsync(BlobProviderGetArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `MyCustomBlobProvider` inherits from the `BlobProviderBase` and overrides the `abstract` methods. The actual implementation is up to you. |
|||
* Implementing `ITransientDependency` registers this class to the [Dependency Injection](Dependency-Injection.md) system as a transient service. |
|||
|
|||
> **Notice: Naming conventions are important**. If your class name doesn't end with `BlobProvider`, you must manually register/expose your service for the `IBlobProvider`. |
|||
|
|||
That's all. Now, you can configure containers (inside the `ConfigureServices` method of your [module](Module-Development-Basics.md)) to use the `MyCustomBlobProvider` class: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.ProviderType = typeof(MyCustomBlobProvider); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) if you want to configure a specific container. |
|||
|
|||
### BlobContainerConfiguration Extension Method |
|||
|
|||
If you want to provide a simpler configuration, create an extension method for the `BlobContainerConfiguration` class: |
|||
|
|||
```` |
|||
public static class MyBlobContainerConfigurationExtensions |
|||
{ |
|||
public static BlobContainerConfiguration UseMyCustomBlobProvider( |
|||
this BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); |
|||
return containerConfiguration; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can configure containers easier using the extension method: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseMyCustomBlobProvider(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### Extra Configuration Options |
|||
|
|||
`BlobContainerConfiguration` allows to add/remove provider specific configuration objects. If your provider needs to additional configuration, you can create a wrapper class to the `BlobContainerConfiguration` for a type-safe configuration option: |
|||
|
|||
````csharp |
|||
public class MyCustomBlobProviderConfiguration |
|||
{ |
|||
public string MyOption1 |
|||
{ |
|||
get => _containerConfiguration |
|||
.GetConfiguration<string>("MyCustomBlobProvider.MyOption1"); |
|||
set => _containerConfiguration |
|||
.SetConfiguration("MyCustomBlobProvider.MyOption1", value); |
|||
} |
|||
|
|||
private readonly BlobContainerConfiguration _containerConfiguration; |
|||
|
|||
public MyCustomBlobProviderConfiguration( |
|||
BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
_containerConfiguration = containerConfiguration; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can change the `MyBlobContainerConfigurationExtensions` class like that: |
|||
|
|||
````csharp |
|||
public static class MyBlobContainerConfigurationExtensions |
|||
{ |
|||
public static BlobContainerConfiguration UseMyCustomBlobProvider( |
|||
this BlobContainerConfiguration containerConfiguration, |
|||
Action<MyCustomBlobProviderConfiguration> configureAction) |
|||
{ |
|||
containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); |
|||
|
|||
configureAction.Invoke( |
|||
new MyCustomBlobProviderConfiguration(containerConfiguration) |
|||
); |
|||
|
|||
return containerConfiguration; |
|||
} |
|||
|
|||
public static MyCustomBlobProviderConfiguration GetMyCustomBlobProviderConfiguration( |
|||
this BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
return new MyCustomBlobProviderConfiguration(containerConfiguration); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Added an action parameter to the `UseMyCustomBlobProvider` method to allow developers to set the additional options. |
|||
* Added a new `GetMyCustomBlobProviderConfiguration` method to be used inside `MyCustomBlobProvider` class to obtain the configured values. |
|||
|
|||
Then anyone can set the `MyOption1` as shown below: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseMyCustomBlobProvider(provider => |
|||
{ |
|||
provider.MyOption1 = "my value"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Finally, you can access to the extra options using the `GetMyCustomBlobProviderConfiguration` method: |
|||
|
|||
````csharp |
|||
public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency |
|||
{ |
|||
public override Task SaveAsync(BlobProviderSaveArgs args) |
|||
{ |
|||
var config = args.Configuration.GetMyCustomBlobProviderConfiguration(); |
|||
var value = config.MyOption1; |
|||
|
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Contribute? |
|||
|
|||
If you create a new provider and you think it can be useful for other developers, please consider to [contribute](Contribution/Index.md) to the ABP Framework on GitHub. |
|||
@ -0,0 +1,96 @@ |
|||
# BLOB Storing Database Provider |
|||
|
|||
BLOB Storing Database Storage Provider can store BLOBs in a relational or non-relational database. |
|||
|
|||
There are two database providers implemented; |
|||
|
|||
* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) package implements for [EF Core](Entity-Framework-Core.md), so it can store BLOBs in [any DBMS supported](https://docs.microsoft.com/en-us/ef/core/providers/) by the EF Core. |
|||
* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) package implements for [MongoDB](MongoDB.md). |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a database as the storage provider. |
|||
|
|||
## Installation |
|||
|
|||
### Automatic Installation |
|||
|
|||
If you've created your solution based on the [application startup template](Startup-Templates/Application.md), you can use the `abp add-module` [CLI](CLI.md) command to automatically add related packages to your solution. |
|||
|
|||
Open a command prompt (terminal) in the folder containing your solution (`.sln`) file and run the following command: |
|||
|
|||
````bash |
|||
abp add-module Volo.Abp.BlobStoring.Database |
|||
```` |
|||
|
|||
This command adds all the NuGet packages to corresponding layers of your solution. If you are using EF Core, it adds necessary configuration, adds a new database migration and updates the database. |
|||
|
|||
### Manual Installation |
|||
|
|||
Here, all the NuGet packages defined by this provider; |
|||
|
|||
* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Domain.Shared) |
|||
* [Volo.Abp.BlobStoring.Database.Domain](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain) |
|||
* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) |
|||
* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) |
|||
|
|||
You can only install Volo.Abp.BlobStoring.Database.EntityFrameworkCore or Volo.Abp.BlobStoring.Database.MongoDB (based on your preference) since they depends on the other packages. |
|||
|
|||
After installation, add `DepenedsOn` attribute to your related [module](Module-Development-Basics.md). Here, the list of module classes defined by the related NuGet packages listed above: |
|||
|
|||
* `BlobStoringDatabaseDomainModule` |
|||
* `BlobStoringDatabaseDomainSharedModule` |
|||
* `BlobStoringDatabaseEntityFrameworkCoreModule` |
|||
* `BlobStoringDatabaseMongoDbModule` |
|||
|
|||
Whenever you add a NuGet package to a project, also add the module class dependency. |
|||
|
|||
If you are using EF Core, you also need to configure your **Migration DbContext** to add BLOB storage tables to your database schema. Call `builder.ConfigureBlobStoring()` extension method inside the `OnModelCreating` method to include mappings to your DbContext. Then you can use the standard `Add-Migration` and `Update-Database` [commands](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create necessary tables in your database. |
|||
|
|||
## Configuration |
|||
|
|||
### Connection String |
|||
|
|||
If you will use your `Default` connection string, you don't need to any additional configuration. |
|||
|
|||
If you want to use a separate database for BLOB storage, use the `AbpBlobStoring` as the [connection string](Connection-Strings.md) name in your configuration file (`appsettings.json`). In this case, also read the [EF Core Migrations](Entity-Framework-Core-Migrations.md) document to learn how to create and use a different database for a desired module. |
|||
|
|||
### Configuring the Containers |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). |
|||
|
|||
**Example: Configure to use the database storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseDatabase(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. |
|||
|
|||
## Additional Information |
|||
|
|||
It is expected to use the [BLOB Storing services](Blob-Storing.md) to use the BLOB storing system. However, if you want to work on the database tables/entities, you can use the following information. |
|||
|
|||
### Entities |
|||
|
|||
Entities defined for this module: |
|||
|
|||
* `DatabaseBlobContainer` (aggregate root) represents a container stored in the database. |
|||
* `DatabaseBlob` (aggregate root) represents a BLOB in the database. |
|||
|
|||
See the [entities document](Entities.md) to learn what is an entity and aggregate root. |
|||
|
|||
### Repositories |
|||
|
|||
* `IDatabaseBlobContainerRepository` |
|||
* `IDatabaseBlobRepository` |
|||
|
|||
You can also use `IRepository<DatabaseBlobContainer, Guid>` and `IRepository<DatabaseBlob, Guid>` to take the power of IQueryable. See the [repository document](Repositories.md) for more. |
|||
|
|||
### Other Services |
|||
|
|||
* `DatabaseBlobProvider` is the main service that implements the database BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `DatabaseBlobProvider` class). |
|||
@ -0,0 +1,59 @@ |
|||
# BLOB Storing File System Provider |
|||
|
|||
File System Storage Provider is used to store BLOBs in the local file system as standard files inside a folder. |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use the file system. |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem) NuGet package to your project: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.FileSystem` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring.FileSystem` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringFileSystemModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## Configuration |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). |
|||
|
|||
**Example: Configure to use the File System storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseFileSystem(fileSystem => |
|||
{ |
|||
fileSystem.BasePath = "C:\\my-files"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
`UseFileSystem` extension method is used to set the File System Provider for a container and configure the file system options. |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. |
|||
|
|||
### Options |
|||
|
|||
* **BasePath** (string): The base folder path to store BLOBs. It is required to set this option. |
|||
* **AppendContainerNameToBasePath** (bool; default: `true`): Indicates whether to create a folder with the container name inside the base folder. If you store multiple containers in the same `BaseFolder`, leave this as `true`. Otherwise, you can set it to `false` if you don't like an unnecessarily deeper folder hierarchy. |
|||
|
|||
## File Path Calculation |
|||
|
|||
File System Provider organizes BLOB files inside folders and implements some conventions. The full path of a BLOB file is determined by the following rules by default: |
|||
|
|||
* It starts with the `BasePath` configured as shown above. |
|||
* Appends `host` folder if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). |
|||
* Appends `tenants/<tenant-id>` folder if current tenant is not `null`. |
|||
* Appends the container's name if `AppendContainerNameToBasePath` is `true`. If container name contains `/`, this will result with nested folders. |
|||
* Appends the BLOB name. If the BLOB name contains `/` it creates folders. If the BLOB name contains `.` it will have a file extension. |
|||
|
|||
## Extending the File System BLOB Provider |
|||
|
|||
* `FileSystemBlobProvider` is the main service that implements the File System storage. You can inherit from this class and [override](Customizing-Application-Modules-Overriding-Services.md) methods to customize it. |
|||
|
|||
* The `IBlobFilePathCalculator` service is used to calculate the file paths. Default implementation is the `DefaultBlobFilePathCalculator`. You can replace/override it if you want to customize the file path calculation. |
|||
@ -1,3 +1,305 @@ |
|||
# Blog Storing |
|||
# BLOB Storing |
|||
|
|||
TODO |
|||
It is typical to **store file contents** in an application and read these file contents on need. Not only files, but you may also need to save various types of **large binary objects**, a.k.a. [BLOB](https://en.wikipedia.org/wiki/Binary_large_object)s, into a **storage**. For example, you may want to save user profile pictures. |
|||
|
|||
A BLOB is a typically **byte array**. There are various places to store a BLOB item; storing in the local file system, in a shared database or on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/) can be options. |
|||
|
|||
The ABP Framework provides an abstraction to work with BLOBs and provides some pre-built storage providers that you can easily integrate to. Having such an abstraction has some benefits; |
|||
|
|||
* You can **easily integrate** to your favorite BLOB storage provides with a few lines of configuration. |
|||
* You can then **easily change** your BLOB storage without changing your application code. |
|||
* If you want to create **reusable application modules**, you don't need to make assumption about how the BLOBs are stored. |
|||
|
|||
ABP BLOB Storage system is also compatible to other ABP Framework features like [multi-tenancy](Multi-Tenancy.md). |
|||
|
|||
## BLOB Storage Providers |
|||
|
|||
The ABP Framework has already the following storage provider implementations; |
|||
|
|||
* [File System](Blob-Storing-File-System.md): Stores BLOBs in a folder of the local file system, as standard files. |
|||
* [Database](Blob-Storing-Database.md): Stores BLOBs in a database. |
|||
* [Azure](Blob-Storing-Azure.md): Stores BLOBs on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/). |
|||
|
|||
More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](Blob-Storing-Custom-Provider.md) and [contribute](Contribution/Index.md) to the ABP Framework. |
|||
|
|||
Multiple providers **can be used together** by the help of the **container system**, where each container can uses a different provider. |
|||
|
|||
> BLOB storing system can not work unless you **configure a storage provider**. Refer to the linked documents for the storage provider configurations. |
|||
|
|||
## Installation |
|||
|
|||
[Volo.Abp.BlobStoring](https://www.nuget.org/packages/Volo.Abp.BlobStoring) is the main package that defines the BLOB storing services. You can use this package to use the BLOB Storing system without depending a specific storage provider. |
|||
|
|||
Use the ABP CLI to add this package to your project: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), if you haven't installed it. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring](https://www.nuget.org/packages/Volo.Abp.BlobStoring) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## The IBlobContainer |
|||
|
|||
`IBlobContainer` is the main interface to store and read BLOBs. Your application may have multiple containers and each container can be separately configured. But, there is a **default container** that can be simply used by [injecting](Dependency-Injection.md) the `IBlobContainer`. |
|||
|
|||
**Example: Simply save and read bytes of a named BLOB** |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBlobContainer _blobContainer; |
|||
|
|||
public MyService(IBlobContainer blobContainer) |
|||
{ |
|||
_blobContainer = blobContainer; |
|||
} |
|||
|
|||
public async Task SaveBytesAsync(byte[] bytes) |
|||
{ |
|||
await _blobContainer.SaveAsync("my-blob-1", bytes); |
|||
} |
|||
|
|||
public async Task<byte[]> GetBytesAsync() |
|||
{ |
|||
return await _blobContainer.GetAllBytesOrNullAsync("my-blob-1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This service saves the given bytes with the `my-blob-1` name and then gets the previously saved bytes with the same name. |
|||
|
|||
> A BLOB is a named object and **each BLOB should have a unique name**, which is an arbitrary string. |
|||
|
|||
`IBlobContainer` can work with `Stream` and `byte[]` objects, which will be detailed in the next sections. |
|||
|
|||
### Saving BLOBs |
|||
|
|||
`SaveAsync` method is used to save a new BLOB or replace an existing BLOB. It can save a `Stream` by default, but there is a shortcut extension method to save byte arrays. |
|||
|
|||
`SaveAsync` gets the following parameters: |
|||
|
|||
* **name** (string): Unique name of the BLOB. |
|||
* **stream** (Stream) or **bytes** (byte[]): The stream to read the BLOB content or a byte array. |
|||
* **overrideExisting** (bool): Set `true` to replace the BLOB content if it does already exists. Default value is `false` and throws `BlobAlreadyExistsException` if there is already a BLOB in the container with the same name. |
|||
|
|||
### Reading/Getting BLOBs |
|||
|
|||
* `GetAsync`: Only gets a BLOB name and returns a `Stream` object that can be used to read the BLOB content. Always **dispose the stream** after using it. This method throws exception, if it can not find the BLOB with the given name. |
|||
* `GetOrNullAsync`: In opposite to the `GetAsync` method, this one returns `null` if there is no BLOB found with the given name. |
|||
* `GetAllBytesAsync`: Returns a `byte[]` instead of a `Stream`. Still throws exception if can not find the BLOB with the given name. |
|||
* `GetAllBytesOrNullAsync`: In opposite to the `GetAllBytesAsync` method, this one returns `null` if there is no BLOB found with the given name. |
|||
|
|||
### Deleting BLOBs |
|||
|
|||
`DeleteAsync` method gets a BLOB name and deletes the BLOB data. It doesn't throw any exception if given BLOB was not found. Instead, it returns a `bool` indicating that the BLOB was actually deleted or not, if you care about it. |
|||
|
|||
### Other Methods |
|||
|
|||
* `ExistsAsync` method simply checks if there is a BLOB in the container with the given name. |
|||
|
|||
### About Naming the BLOBs |
|||
|
|||
There is not a rule for naming the BLOBs. A BLOB name is just a string that is unique per container (and per tenant - see the "*Multi-Tenancy*" section). However, different storage providers may conventionally implement some practices. For example, the [File System Provider](Blob-Storing-File-System.md) use directory separators (`/`) and file extensions in your BLOB name (if your BLOB name is `images/common/x.png` then it is saved as `x.png` in the `images/common` folder inside the root container folder). |
|||
|
|||
## Typed IBlobContainer |
|||
|
|||
Typed BLOB container system is a way of creating and managing **multiple containers** in an application; |
|||
|
|||
* **Each container is separately stored**. That means the BLOB names should be unique in a container and two BLOBs with the same name can live in different containers without effecting each other. |
|||
* **Each container can be separately configured**, so each container can use a different storage provider based on your configuration. |
|||
|
|||
To create a typed container, you need to create a simple class decorated with the `BlobContainerName` attribute: |
|||
|
|||
````csharp |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
[BlobContainerName("profile-pictures")] |
|||
public class ProfilePictureContainer |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
> If you don't use the `BlobContainerName` attribute, ABP Framework uses the full name of the class (with namespace), but it is always recommended to use a container name which is stable and does not change even if you rename the class. |
|||
|
|||
Once you create the container class, you can inject `IBlobContainer<T>` for your container type. |
|||
|
|||
**Example: An [application service](Application-Services.md) to save and read profile picture of the [current user](CurrentUser.md)** |
|||
|
|||
````csharp |
|||
[Authorize] |
|||
public class ProfileAppService : ApplicationService |
|||
{ |
|||
private readonly IBlobContainer<ProfilePictureContainer> _blobContainer; |
|||
|
|||
public ProfileAppService(IBlobContainer<ProfilePictureContainer> blobContainer) |
|||
{ |
|||
_blobContainer = blobContainer; |
|||
} |
|||
|
|||
public async Task SaveProfilePictureAsync(byte[] bytes) |
|||
{ |
|||
var blobName = CurrentUser.GetId().ToString(); |
|||
await _blobContainer.SaveAsync(blobName, bytes); |
|||
} |
|||
|
|||
public async Task<byte[]> GetProfilePictureAsync() |
|||
{ |
|||
var blobName = CurrentUser.GetId().ToString(); |
|||
return await _blobContainer.GetAllBytesOrNullAsync(blobName); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`IBlobContainer<T>` has the same methods with the `IBlobContainer`. |
|||
|
|||
> It is a good practice to **always use a typed container while developing re-usable modules**, so the final application can configure the provider for your container without effecting the other containers. |
|||
|
|||
### The Default Container |
|||
|
|||
If you don't use the generic argument and directly inject the `IBlobContainer` (as explained before), you get the default container. Another way of injecting the default container is using `IBlobContainer<DefaultContainer>`, which returns exactly the same container. |
|||
|
|||
The name of the default container is `Default`. |
|||
|
|||
### Named Containers |
|||
|
|||
Typed containers are just shortcuts for named containers. You can inject and use the `IBlobContainerFactory` to get a BLOB container by its name: |
|||
|
|||
````csharp |
|||
public class ProfileAppService : ApplicationService |
|||
{ |
|||
private readonly IBlobContainer _blobContainer; |
|||
|
|||
public ProfileAppService(IBlobContainerFactory blobContainerFactory) |
|||
{ |
|||
_blobContainer = blobContainerFactory.Create("profile-pictures"); |
|||
} |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
## IBlobContainerFactory |
|||
|
|||
`IBlobContainerFactory` is the service that is used to create the BLOB containers. One example was shown above. |
|||
|
|||
**Example: Create a container by name** |
|||
|
|||
````csharp |
|||
var blobContainer = blobContainerFactory.Create("profile-pictures"); |
|||
```` |
|||
|
|||
**Example: Create a container by type** |
|||
|
|||
````csharp |
|||
var blobContainer = blobContainerFactory.Create<ProfilePictureContainer>(); |
|||
```` |
|||
|
|||
> You generally don't need to use the `IBlobContainerFactory` since it is used internally, when you inject a `IBlobContainer` or `IBlobContainer<T>`. |
|||
|
|||
## Configuring the Containers |
|||
|
|||
Containers should be configured before using them. The most fundamental configuration is to **select a BLOB storage provider** (see the "*BLOB Storage Providers*" section above). |
|||
|
|||
`AbpBlobStoringOptions` is the [options class](Options.md) to configure the containers. You can configure the options inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). |
|||
|
|||
### Configure a Single Container |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure<ProfilePictureContainer>(container => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
This example configures the `ProfilePictureContainer`. You can also configure by the container name: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure("profile-pictures", container => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### Configure the Default Container |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> There is a special case about the default container; If you don't specify a configuration for a container, it **fallbacks to the default container configuration**. This is a good way to configure defaults for all containers and specialize configuration for a specific container when needed. |
|||
|
|||
### Configure All Containers |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureAll((containerName, containerConfiguration) => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
This is a way to configure all the containers. |
|||
|
|||
> The main difference from configuring the default container is that `ConfigureAll` overrides the configuration even if it was specialized for a specific container. |
|||
|
|||
## Multi-Tenancy |
|||
|
|||
If your application is set as multi-tenant, the BLOB Storage system **works seamlessly with the [multi-tenancy](Multi-Tenancy.md)**. All the providers implement multi-tenancy as a standard feature. They **isolate BLOBs** of different tenants from each other, so they can only access to their own BLOBs. It means you can use the **same BLOB name for different tenants**. |
|||
|
|||
If your application is multi-tenant, you may want to control **multi-tenancy behavior** of the containers individually. For example, you may want to **disable multi-tenancy** for a specific container, so the BLOBs inside it will be **available to all the tenants**. This is a way to share BLOBs among all tenants. |
|||
|
|||
**Example: Disable multi-tenancy for a specific container** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure<ProfilePictureContainer>(container => |
|||
{ |
|||
container.IsMultiTenant = false; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> If your application is not multi-tenant, no worry, it works as expected. You don't need to configure the `IsMultiTenant` option. |
|||
|
|||
## Extending the BLOB Storing System |
|||
|
|||
Most of the times, you won't need to customize the BLOB storage system except [creating a custom BLOB storage provider](Blob-Storing-Custom-Provider.md). However, you can replace any service (injected via [dependency injection](Dependency-Injection.md)), if you need. Here, some other services not mentioned above, but you may want to know: |
|||
|
|||
* `IBlobProviderSelector` is used to get a `IBlobProvider` instance by a container name. Default implementation (`DefaultBlobProviderSelector`) selects the provider using the configuration. |
|||
* `IBlobContainerConfigurationProvider` is used to get the `BlobContainerConfiguration` for a given container name. Default implementation (`DefaultBlobContainerConfigurationProvider`) gets the configuration from the `AbpBlobStoringOptions` explained above. |
|||
|
|||
## BLOB Storing vs File Management System |
|||
|
|||
Notice that BLOB storing is not a file management system. It is a low level system that is used to save, get and delete named BLOBs. It doesn't provide a hierarchical structure like directories, you may expect from a typical file system. |
|||
|
|||
If you want to create folders and move files between folders, assign permissions to files and share files between users then you need to implement your own application on top of the BLOB Storage system. |
|||
|
|||
## See Also |
|||
|
|||
* [Creating a custom BLOB storage provider](Blob-Storing-Custom-Provider.md) |
|||
|
|||
@ -1,3 +1,167 @@ |
|||
# Current User |
|||
|
|||
TODO! |
|||
It is very common to retrieve the information about the logged in user in a web application. The current user is the active user related to the current request in a web application. |
|||
|
|||
## ICurrentUser |
|||
|
|||
`ICurrentUser` is the main service to get info about the current active user. |
|||
|
|||
Example: [Injecting](Dependency-Injection.md) the `ICurrentUser` into a service: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ICurrentUser _currentUser; |
|||
|
|||
public MyService(ICurrentUser currentUser) |
|||
{ |
|||
_currentUser = currentUser; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
Guid? userId = _currentUser.Id; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Common base classes have already injected this service as a base property. For example, you can directly use the `CurrentUser` property in an [application service](Application-Services.md): |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyAppService : ApplicationService |
|||
{ |
|||
public void Foo() |
|||
{ |
|||
Guid? userId = CurrentUser.Id; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Properties |
|||
|
|||
Here are the fundamental properties of the `ICurrentUser` interface: |
|||
|
|||
* **IsAuthenticated** (bool): Returns `true` if the current user has logged in (authenticated). If the user has not logged in then `Id` and `UserName` returns `null`. |
|||
* **Id** (Guid?): Id of the current user. Returns `null`, if the current user has not logged in. |
|||
* **UserName** (string): User name of the current user. Returns `null`, if the current user has not logged in. |
|||
* **TenantId** (Guid?): Tenant Id of the current user, which can be useful for a [multi-tenant](Multi-Tenancy.md) application. Returns `null`, if the current user is not assigned to a tenant. |
|||
* **Email** (string): Email address of the current user.Returns `null`, if the current user has not logged in or not set an email address. |
|||
* **EmailVerified** (bool): Returns `true`, if the phone number of the current user has been verified. |
|||
* **PhoneNumber** (string): Phone number of the current user. Returns `null`, if the current user has not logged in or not set a phone number. |
|||
* **PhoneNumberVerified** (bool): Returns `true`, if the phone number of the current user has been verified. |
|||
* **Roles** (string[]): Roles of the current user. Returns a string array of the role names of the current user. |
|||
|
|||
### Methods |
|||
|
|||
`ICurrentUser` is implemented on the `ICurrentPrincipalAccessor` (see the section below) and works with the claims. So, all of the above properties are actually retrieved from the claims of the current authenticated user. |
|||
|
|||
`ICurrentUser` has some methods to directly work with the claims, if you have custom claims or get other non-common claim types. |
|||
|
|||
* **FindClaim**: Gets a claim with the given name. Returns `null` if not found. |
|||
* **FindClaims**: Gets all the claims with the given name (it is allowed to have multiple claim values with the same name). |
|||
* **GetAllClaims**: Gets all the claims. |
|||
* **IsInRole**: A shortcut method to check if the current user is in the specified role. |
|||
|
|||
Beside these standard methods, there are some extension methods: |
|||
|
|||
* **FindClaimValue**: Gets the value of the claim with the given name, or `null` if not found. It has a generic overload that also casts the value to a specific type. |
|||
* **GetId**: Returns `Id` of the current user. If the current user has not logged in, it throws an exception (instead of returning `null`) . Use this only if you are sure that the user has already authenticated in your code context. |
|||
|
|||
### Authentication & Authorization |
|||
|
|||
`ICurrentUser` works independently of how the user is authenticated or authorized. It seamlessly works with any authentication system that works with the current principal (see the section below). |
|||
|
|||
## ICurrentPrincipalAccessor |
|||
|
|||
`ICurrentPrincipalAccessor` is the service that should be used (by the ABP Framework and your application code) whenever the current principle of the current user is needed. |
|||
|
|||
For a web application, it gets the `User` property of the current `HttpContext`. For a non-web application, it returns the `Thread.CurrentPrincipal`. |
|||
|
|||
> You generally don't need to this low level `ICurrentPrincipalAccessor` service and directly work with the `ICurrentUser` explained above. |
|||
|
|||
### Basic Usage |
|||
|
|||
You can inject `ICurrentPrincipalAccessor` and use the `Principal` property to the the current principal: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; |
|||
|
|||
public MyService(ICurrentPrincipalAccessor currentPrincipalAccessor) |
|||
{ |
|||
_currentPrincipalAccessor = currentPrincipalAccessor; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
var allClaims = _currentPrincipalAccessor.Principal.Claims.ToList(); |
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Changing the Current Principle |
|||
|
|||
Current principle is not something you want to set or change, except at some advanced scenarios. If you need it, use the `Change` method of the `ICurrentPrincipalAccessor`. It takes a `ClaimsPrinciple` object and makes it "current" for a scope. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MyAppService : ApplicationService |
|||
{ |
|||
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; |
|||
|
|||
public MyAppService(ICurrentPrincipalAccessor currentPrincipalAccessor) |
|||
{ |
|||
_currentPrincipalAccessor = currentPrincipalAccessor; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
var newPrinciple = new ClaimsPrincipal( |
|||
new ClaimsIdentity( |
|||
new Claim[] |
|||
{ |
|||
new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString()), |
|||
new Claim(AbpClaimTypes.UserName, "john"), |
|||
new Claim("MyCustomCliam", "42") |
|||
} |
|||
) |
|||
); |
|||
|
|||
using (_currentPrincipalAccessor.Change(newPrinciple)) |
|||
{ |
|||
var userName = CurrentUser.UserName; //returns "john" |
|||
//... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Use the `Change` method always in a `using` statement, so it will be restored to the original value after the `using` scope ends. |
|||
|
|||
This can be a way to simulate a user login for a scope of the application code, however try to use it carefully. |
|||
|
|||
## AbpClaimTypes |
|||
|
|||
`AbpClaimTypes` is a static class that defines the names of the standard claims and used by the ABP Framework. |
|||
|
|||
* Default values for the `UserName`, `UserId`, `Role` and `Email` properties are set from the [System.Security.Claims.ClaimTypes](https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes) class, but you can change them. |
|||
* Other properties, like `EmailVerified`, `PhoneNumber`, `TenantId`... are defined by the ABP Framework by following the standard names wherever possible. |
|||
|
|||
It is suggested to use properties of this class instead of magic strings for claim names. |
|||
|
|||
|
|||
@ -0,0 +1,21 @@ |
|||
# Console Application Startup Template |
|||
|
|||
This template is used to create a minimalist console application project. |
|||
|
|||
## How to Start With? |
|||
|
|||
First, install the [ABP CLI](../CLI.md) if you haven't installed before: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
Then use the `abp new` command in an empty folder to create a new solution: |
|||
|
|||
````bash |
|||
abp new Acme.MyConsoleApp -t console |
|||
```` |
|||
|
|||
`Acme.MyConsoleApp` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. |
|||
|
|||
### |
|||
@ -0,0 +1,500 @@ |
|||
# How to Replace PermissionManagementComponent |
|||
|
|||
 |
|||
|
|||
Run the following command in `angular` folder to create a new component called `PermissionManagementComponent`. |
|||
|
|||
```bash |
|||
yarn ng generate component permission-management --entryComponent --inlineStyle |
|||
|
|||
# You don't need the --entryComponent option in Angular 9 |
|||
``` |
|||
|
|||
Open the generated `permission-management.component.ts` in `src/app/permission-management` folder and replace the content with the following: |
|||
|
|||
```js |
|||
import { |
|||
Component, |
|||
EventEmitter, |
|||
Input, |
|||
Output, |
|||
Renderer2, |
|||
TrackByFunction, |
|||
Inject, |
|||
Optional, |
|||
} from '@angular/core'; |
|||
import { ReplaceableComponents } from '@abp/ng.core'; |
|||
import { Select, Store } from '@ngxs/store'; |
|||
import { Observable } from 'rxjs'; |
|||
import { finalize, map, pluck, take, tap } from 'rxjs/operators'; |
|||
import { |
|||
GetPermissions, |
|||
UpdatePermissions, |
|||
PermissionManagement, |
|||
PermissionManagementState, |
|||
} from '@abp/ng.permission-management'; |
|||
|
|||
type PermissionWithMargin = PermissionManagement.Permission & { |
|||
margin: number; |
|||
}; |
|||
|
|||
@Component({ |
|||
selector: 'app-permission-management', |
|||
templateUrl: './permission-management.component.html', |
|||
styles: [ |
|||
` |
|||
.overflow-scroll { |
|||
max-height: 70vh; |
|||
overflow-y: scroll; |
|||
} |
|||
`, |
|||
], |
|||
}) |
|||
export class PermissionManagementComponent |
|||
implements |
|||
PermissionManagement.PermissionManagementComponentInputs, |
|||
PermissionManagement.PermissionManagementComponentOutputs { |
|||
protected _providerName: string; |
|||
@Input() |
|||
get providerName(): string { |
|||
if (this.replaceableData) return this.replaceableData.inputs.providerName; |
|||
|
|||
return this._providerName; |
|||
} |
|||
|
|||
set providerName(value: string) { |
|||
this._providerName = value; |
|||
} |
|||
|
|||
protected _providerKey: string; |
|||
@Input() |
|||
get providerKey(): string { |
|||
if (this.replaceableData) return this.replaceableData.inputs.providerKey; |
|||
|
|||
return this._providerKey; |
|||
} |
|||
|
|||
set providerKey(value: string) { |
|||
this._providerKey = value; |
|||
} |
|||
|
|||
protected _hideBadges = false; |
|||
@Input() |
|||
get hideBadges(): boolean { |
|||
if (this.replaceableData) return this.replaceableData.inputs.hideBadges; |
|||
|
|||
return this._hideBadges; |
|||
} |
|||
|
|||
set hideBadges(value: boolean) { |
|||
this._hideBadges = value; |
|||
} |
|||
|
|||
protected _visible = false; |
|||
@Input() |
|||
get visible(): boolean { |
|||
return this._visible; |
|||
} |
|||
|
|||
set visible(value: boolean) { |
|||
if (value === this._visible) return; |
|||
|
|||
if (value) { |
|||
this.openModal().subscribe(() => { |
|||
this._visible = true; |
|||
this.visibleChange.emit(true); |
|||
if (this.replaceableData) this.replaceableData.outputs.visibleChange(true); |
|||
}); |
|||
} else { |
|||
this.selectedGroup = null; |
|||
this._visible = false; |
|||
this.visibleChange.emit(false); |
|||
if (this.replaceableData) this.replaceableData.outputs.visibleChange(false); |
|||
} |
|||
} |
|||
|
|||
@Output() readonly visibleChange = new EventEmitter<boolean>(); |
|||
|
|||
@Select(PermissionManagementState.getPermissionGroups) |
|||
groups$: Observable<PermissionManagement.Group[]>; |
|||
|
|||
@Select(PermissionManagementState.getEntityDisplayName) |
|||
entityName$: Observable<string>; |
|||
|
|||
selectedGroup: PermissionManagement.Group; |
|||
|
|||
permissions: PermissionManagement.Permission[] = []; |
|||
|
|||
selectThisTab = false; |
|||
|
|||
selectAllTab = false; |
|||
|
|||
modalBusy = false; |
|||
|
|||
trackByFn: TrackByFunction<PermissionManagement.Group> = (_, item) => item.name; |
|||
|
|||
get selectedGroupPermissions$(): Observable<PermissionWithMargin[]> { |
|||
return this.groups$.pipe( |
|||
map((groups) => |
|||
this.selectedGroup |
|||
? groups.find((group) => group.name === this.selectedGroup.name).permissions |
|||
: [] |
|||
), |
|||
map<PermissionManagement.Permission[], PermissionWithMargin[]>((permissions) => |
|||
permissions.map( |
|||
(permission) => |
|||
(({ |
|||
...permission, |
|||
margin: findMargin(permissions, permission), |
|||
isGranted: this.permissions.find((per) => per.name === permission.name).isGranted, |
|||
} as any) as PermissionWithMargin) |
|||
) |
|||
) |
|||
); |
|||
} |
|||
|
|||
get isVisible(): boolean { |
|||
if (!this.replaceableData) return this.visible; |
|||
|
|||
return this.replaceableData.inputs.visible; |
|||
} |
|||
|
|||
constructor( |
|||
@Optional() |
|||
@Inject('REPLACEABLE_DATA') |
|||
public replaceableData: ReplaceableComponents.ReplaceableTemplateData< |
|||
PermissionManagement.PermissionManagementComponentInputs, |
|||
PermissionManagement.PermissionManagementComponentOutputs |
|||
>, |
|||
private store: Store |
|||
) {} |
|||
|
|||
getChecked(name: string) { |
|||
return (this.permissions.find((per) => per.name === name) || { isGranted: false }).isGranted; |
|||
} |
|||
|
|||
isGrantedByOtherProviderName(grantedProviders: PermissionManagement.GrantedProvider[]): boolean { |
|||
if (grantedProviders.length) { |
|||
return grantedProviders.findIndex((p) => p.providerName !== this.providerName) > -1; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
onClickCheckbox(clickedPermission: PermissionManagement.Permission, value) { |
|||
if ( |
|||
clickedPermission.isGranted && |
|||
this.isGrantedByOtherProviderName(clickedPermission.grantedProviders) |
|||
) |
|||
return; |
|||
|
|||
setTimeout(() => { |
|||
this.permissions = this.permissions.map((per) => { |
|||
if (clickedPermission.name === per.name) { |
|||
return { ...per, isGranted: !per.isGranted }; |
|||
} else if (clickedPermission.name === per.parentName && clickedPermission.isGranted) { |
|||
return { ...per, isGranted: false }; |
|||
} else if (clickedPermission.parentName === per.name && !clickedPermission.isGranted) { |
|||
return { ...per, isGranted: true }; |
|||
} |
|||
|
|||
return per; |
|||
}); |
|||
|
|||
this.setTabCheckboxState(); |
|||
this.setGrantCheckboxState(); |
|||
}, 0); |
|||
} |
|||
|
|||
setTabCheckboxState() { |
|||
this.selectedGroupPermissions$.pipe(take(1)).subscribe((permissions) => { |
|||
const selectedPermissions = permissions.filter((per) => per.isGranted); |
|||
const element = document.querySelector('#select-all-in-this-tabs') as any; |
|||
|
|||
if (selectedPermissions.length === permissions.length) { |
|||
element.indeterminate = false; |
|||
this.selectThisTab = true; |
|||
} else if (selectedPermissions.length === 0) { |
|||
element.indeterminate = false; |
|||
this.selectThisTab = false; |
|||
} else { |
|||
element.indeterminate = true; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
setGrantCheckboxState() { |
|||
const selectedAllPermissions = this.permissions.filter((per) => per.isGranted); |
|||
const checkboxElement = document.querySelector('#select-all-in-all-tabs') as any; |
|||
|
|||
if (selectedAllPermissions.length === this.permissions.length) { |
|||
checkboxElement.indeterminate = false; |
|||
this.selectAllTab = true; |
|||
} else if (selectedAllPermissions.length === 0) { |
|||
checkboxElement.indeterminate = false; |
|||
this.selectAllTab = false; |
|||
} else { |
|||
checkboxElement.indeterminate = true; |
|||
} |
|||
} |
|||
|
|||
onClickSelectThisTab() { |
|||
this.selectedGroupPermissions$.pipe(take(1)).subscribe((permissions) => { |
|||
permissions.forEach((permission) => { |
|||
if (permission.isGranted && this.isGrantedByOtherProviderName(permission.grantedProviders)) |
|||
return; |
|||
|
|||
const index = this.permissions.findIndex((per) => per.name === permission.name); |
|||
|
|||
this.permissions = [ |
|||
...this.permissions.slice(0, index), |
|||
{ ...this.permissions[index], isGranted: !this.selectThisTab }, |
|||
...this.permissions.slice(index + 1), |
|||
]; |
|||
}); |
|||
}); |
|||
|
|||
this.setGrantCheckboxState(); |
|||
} |
|||
|
|||
onClickSelectAll() { |
|||
this.permissions = this.permissions.map((permission) => ({ |
|||
...permission, |
|||
isGranted: |
|||
this.isGrantedByOtherProviderName(permission.grantedProviders) || !this.selectAllTab, |
|||
})); |
|||
|
|||
this.selectThisTab = !this.selectAllTab; |
|||
} |
|||
|
|||
onChangeGroup(group: PermissionManagement.Group) { |
|||
this.selectedGroup = group; |
|||
this.setTabCheckboxState(); |
|||
} |
|||
|
|||
submit() { |
|||
this.modalBusy = true; |
|||
const unchangedPermissions = getPermissions( |
|||
this.store.selectSnapshot(PermissionManagementState.getPermissionGroups) |
|||
); |
|||
|
|||
const changedPermissions: PermissionManagement.MinimumPermission[] = this.permissions |
|||
.filter((per) => |
|||
unchangedPermissions.find((unchanged) => unchanged.name === per.name).isGranted === |
|||
per.isGranted |
|||
? false |
|||
: true |
|||
) |
|||
.map(({ name, isGranted }) => ({ name, isGranted })); |
|||
|
|||
if (changedPermissions.length) { |
|||
this.store |
|||
.dispatch( |
|||
new UpdatePermissions({ |
|||
providerKey: this.providerKey, |
|||
providerName: this.providerName, |
|||
permissions: changedPermissions, |
|||
}) |
|||
) |
|||
.pipe(finalize(() => (this.modalBusy = false))) |
|||
.subscribe(() => { |
|||
this.visible = false; |
|||
}); |
|||
} else { |
|||
this.modalBusy = false; |
|||
this.visible = false; |
|||
} |
|||
} |
|||
|
|||
openModal() { |
|||
if (!this.providerKey || !this.providerName) { |
|||
throw new Error('Provider Key and Provider Name are required.'); |
|||
} |
|||
|
|||
return this.store |
|||
.dispatch( |
|||
new GetPermissions({ |
|||
providerKey: this.providerKey, |
|||
providerName: this.providerName, |
|||
}) |
|||
) |
|||
.pipe( |
|||
pluck('PermissionManagementState', 'permissionRes'), |
|||
tap((permissionRes: PermissionManagement.Response) => { |
|||
this.selectedGroup = permissionRes.groups[0]; |
|||
this.permissions = getPermissions(permissionRes.groups); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
initModal() { |
|||
this.setTabCheckboxState(); |
|||
this.setGrantCheckboxState(); |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean) { |
|||
this.visible = visible; |
|||
|
|||
if (this.replaceableData) { |
|||
this.replaceableData.inputs.visible = visible; |
|||
this.replaceableData.outputs.visibleChange(visible); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function findMargin( |
|||
permissions: PermissionManagement.Permission[], |
|||
permission: PermissionManagement.Permission |
|||
) { |
|||
const parentPermission = permissions.find((per) => per.name === permission.parentName); |
|||
|
|||
if (parentPermission && parentPermission.parentName) { |
|||
let margin = 20; |
|||
return (margin += findMargin(permissions, parentPermission)); |
|||
} |
|||
|
|||
return parentPermission ? 20 : 0; |
|||
} |
|||
|
|||
function getPermissions(groups: PermissionManagement.Group[]): PermissionManagement.Permission[] { |
|||
return groups.reduce((acc, val) => [...acc, ...val.permissions], []); |
|||
} |
|||
``` |
|||
|
|||
Open the generated `permission-management.component.html` in `src/app/permission-management` folder and replace the content with the below: |
|||
|
|||
```html |
|||
<abp-modal |
|||
[visible]="isVisible" |
|||
(visibleChange)="onVisibleChange($event)" |
|||
(init)="initModal()" |
|||
[busy]="modalBusy" |
|||
> |
|||
<ng-container *ngIf="{ entityName: entityName$ | async } as data"> |
|||
<ng-template #abpHeader> |
|||
<h4> |
|||
{%{{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}}%} - {%{{{ data.entityName }}}%} |
|||
</h4> |
|||
</ng-template> |
|||
<ng-template #abpBody> |
|||
<div class="custom-checkbox custom-control mb-2"> |
|||
<input |
|||
type="checkbox" |
|||
id="select-all-in-all-tabs" |
|||
name="select-all-in-all-tabs" |
|||
class="custom-control-input" |
|||
[(ngModel)]="selectAllTab" |
|||
(click)="onClickSelectAll()" |
|||
/> |
|||
<label class="custom-control-label" for="select-all-in-all-tabs">{%{{{ |
|||
'AbpPermissionManagement::SelectAllInAllTabs' | abpLocalization |
|||
}}}%}</label> |
|||
</div> |
|||
|
|||
<hr class="mt-2 mb-2" /> |
|||
<div class="row"> |
|||
<div class="overflow-scroll col-md-4"> |
|||
<ul class="nav nav-pills flex-column"> |
|||
<li *ngFor="let group of groups$ | async; trackBy: trackByFn" class="nav-item"> |
|||
<a |
|||
class="nav-link pointer" |
|||
[class.active]="selectedGroup?.name === group?.name" |
|||
(click)="onChangeGroup(group)" |
|||
>{%{{{ group?.displayName }}}%}</a |
|||
> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<div class="col-md-8 overflow-scroll"> |
|||
<h4>{%{{{ selectedGroup?.displayName }}}%}</h4> |
|||
<hr class="mt-2 mb-3" /> |
|||
<div class="pl-1 pt-1"> |
|||
<div class="custom-checkbox custom-control mb-2"> |
|||
<input |
|||
type="checkbox" |
|||
id="select-all-in-this-tabs" |
|||
name="select-all-in-this-tabs" |
|||
class="custom-control-input" |
|||
[(ngModel)]="selectThisTab" |
|||
(click)="onClickSelectThisTab()" |
|||
/> |
|||
<label class="custom-control-label" for="select-all-in-this-tabs">{%{{{ |
|||
'AbpPermissionManagement::SelectAllInThisTab' | abpLocalization |
|||
}}}%}</label> |
|||
</div> |
|||
<hr class="mb-3" /> |
|||
<div |
|||
*ngFor=" |
|||
let permission of selectedGroupPermissions$ | async; |
|||
let i = index; |
|||
trackBy: trackByFn |
|||
" |
|||
[style.margin-left]="permission.margin + 'px'" |
|||
class="custom-checkbox custom-control mb-2" |
|||
> |
|||
<input |
|||
#permissionCheckbox |
|||
type="checkbox" |
|||
[checked]="getChecked(permission.name)" |
|||
[value]="getChecked(permission.name)" |
|||
[attr.id]="permission.name" |
|||
class="custom-control-input" |
|||
[disabled]="isGrantedByOtherProviderName(permission.grantedProviders)" |
|||
/> |
|||
<label |
|||
class="custom-control-label" |
|||
[attr.for]="permission.name" |
|||
(click)="onClickCheckbox(permission, permissionCheckbox.value)" |
|||
>{%{{{ permission.displayName }}}%} |
|||
<ng-container *ngIf="!hideBadges"> |
|||
<span |
|||
*ngFor="let provider of permission.grantedProviders" |
|||
class="badge badge-light" |
|||
>{%{{{ provider.providerName }}}%}: {%{{{ provider.providerKey }}}%}</span |
|||
> |
|||
</ng-container> |
|||
</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" #abpClose> |
|||
{%{{{ 'AbpIdentity::Cancel' | abpLocalization }}}%} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" (click)="submit()">{%{{{ |
|||
'AbpIdentity::Save' | abpLocalization |
|||
}}}%}</abp-button> |
|||
</ng-template> |
|||
</ng-container> |
|||
</abp-modal> |
|||
``` |
|||
|
|||
Open `app.component.ts` in `src/app` folder and modify it as shown below: |
|||
|
|||
```js |
|||
import { AddReplaceableComponent } from '@abp/ng.core'; |
|||
import { ePermissionManagementComponents } from '@abp/ng.permission-management'; |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { PermissionManagementComponent } from './permission-management/permission-management.component'; |
|||
|
|||
//... |
|||
export class AppComponent implements OnInit { |
|||
constructor(private store: Store) {} // injected store |
|||
|
|||
ngOnInit() { |
|||
// added dispatching the AddReplaceableComponent action |
|||
this.store.dispatch( |
|||
new AddReplaceableComponent({ |
|||
component: PermissionManagementComponent, |
|||
key: ePermissionManagementComponents.PermissionManagement, |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## See Also |
|||
|
|||
- [Component Replacement](./Component-Replacement.md) |
|||
|
After Width: | Height: | Size: 252 KiB |
@ -0,0 +1,40 @@ |
|||
# Badges |
|||
|
|||
## Introduction |
|||
|
|||
`abp-badge` and `abp-badge-pill` are abp tags for badges. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<span abp-badge="Primary">Primary</span> |
|||
<a abp-badge="Info" href="#">Info</a> |
|||
<a abp-badge-pill="Danger" href="#">Danger</a> |
|||
```` |
|||
|
|||
|
|||
|
|||
## Demo |
|||
|
|||
See the [badges demo page](https://bootstrap-taghelpers.abp.io/Components/Badges) to see it in action. |
|||
|
|||
### Values |
|||
|
|||
* Indicates the type of the badge. Should be one of the following values: |
|||
|
|||
* `_` (default value) |
|||
* `Default` (default value) |
|||
* `Primary` |
|||
* `Secondary` |
|||
* `Success` |
|||
* `Danger` |
|||
* `Warning` |
|||
* `Info` |
|||
* `Light` |
|||
* `Dark` |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
<span abp-badge-pill="Danger">Danger</span> |
|||
```` |
|||
@ -0,0 +1,126 @@ |
|||
# Borders |
|||
|
|||
## Introduction |
|||
|
|||
`abp-border` is a main element for border styling. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<span abp-border="Default"></span> |
|||
<span abp-border="Top"></span> |
|||
<span abp-border="Right"></span> |
|||
<span abp-border="Bottom"></span> |
|||
<span abp-border="Left"></span> |
|||
```` |
|||
|
|||
|
|||
|
|||
## Demo |
|||
|
|||
See the [borders demo page](https://bootstrap-taghelpers.abp.io/Components/Borders) to see it in action. |
|||
|
|||
## Values |
|||
|
|||
A value indicates type, position and the color of the border. Should be one of the following values: |
|||
|
|||
* `Default` |
|||
* `_0` |
|||
* `Primary` |
|||
* `Secondary` |
|||
* `Success` |
|||
* `Danger` |
|||
* `Warning` |
|||
* `Info` |
|||
* `Light` |
|||
* `Dark` |
|||
* `White` |
|||
* `Primary_0` |
|||
* `Secondary_0` |
|||
* `Success_0` |
|||
* `Danger_0` |
|||
* `Warning_0` |
|||
* `Info_0` |
|||
* `Light_0` |
|||
* `Dark_0` |
|||
* `White_0` |
|||
* `Top` |
|||
* `Top_0` |
|||
* `Top_Primary` |
|||
* `Top_Secondary` |
|||
* `Top_Success` |
|||
* `Top_Danger` |
|||
* `Top_Warning` |
|||
* `Top_Info` |
|||
* `Top_Light` |
|||
* `Top_Dark` |
|||
* `Top_White` |
|||
* `Top_Primary_0` |
|||
* `Top_Secondary_0` |
|||
* `Top_Success_0` |
|||
* `Top_Danger_0` |
|||
* `Top_Warning_0` |
|||
* `Top_Info_0` |
|||
* `Top_Light_0` |
|||
* `Top_Dark_0` |
|||
* `Top_White_0` |
|||
* `Right` |
|||
* `Right_0` |
|||
* `Right_Primary` |
|||
* `Right_Secondary` |
|||
* `Right_Success` |
|||
* `Right_Danger` |
|||
* `Right_Warning` |
|||
* `Right_Info` |
|||
* `Right_Light` |
|||
* `Right_Dark` |
|||
* `Right_White` |
|||
* `Right_Primary_0` |
|||
* `Right_Secondary_0` |
|||
* `Right_Success_0` |
|||
* `Right_Danger_0` |
|||
* `Right_Warning_0` |
|||
* `Right_Info_0` |
|||
* `Right_Light_0` |
|||
* `Right_Dark_0` |
|||
* `Right_White_0` |
|||
* `Left` |
|||
* `Left_0` |
|||
* `Left_Primary` |
|||
* `Left_Secondary` |
|||
* `Left_Success` |
|||
* `Left_Danger` |
|||
* `Left_Warning` |
|||
* `Left_Info` |
|||
* `Left_Light` |
|||
* `Left_Dark` |
|||
* `Left_White` |
|||
* `Left_Primary_0` |
|||
* `Left_Secondary_0` |
|||
* `Left_Success_0` |
|||
* `Left_Danger_0` |
|||
* `Left_Warning_0` |
|||
* `Left_Info_0` |
|||
* `Left_Light_0` |
|||
* `Left_Dark_0` |
|||
* `Left_White_0` |
|||
* `Bottom` |
|||
* `Bottom_0` |
|||
* `Bottom_Primary` |
|||
* `Bottom_Secondary` |
|||
* `Bottom_Success` |
|||
* `Bottom_Danger` |
|||
* `Bottom_Warning` |
|||
* `Bottom_Info` |
|||
* `Bottom_Light` |
|||
* `Bottom_Dark` |
|||
* `Bottom_White` |
|||
* `Bottom_Primary_0` |
|||
* `Bottom_Secondary_0` |
|||
* `Bottom_Success_0` |
|||
* `Bottom_Danger_0` |
|||
* `Bottom_Warning_0` |
|||
* `Bottom_Info_0` |
|||
* `Bottom_Light_0` |
|||
* `Bottom_Dark_0` |
|||
* `Bottom_White_0` |
|||
@ -0,0 +1,25 @@ |
|||
# Breadcrumbs |
|||
|
|||
## Introduction |
|||
|
|||
`abp-breadcrumb` is the main container for breadcrumb items. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<abp-breadcrumb> |
|||
<abp-breadcrumb-item href="#" title="Home" /> |
|||
<abp-breadcrumb-item href="#" title="Library"/> |
|||
<abp-breadcrumb-item title="Page"/> |
|||
</abp-breadcrumb> |
|||
```` |
|||
|
|||
## Demo |
|||
|
|||
See the [breadcrumbs demo page](https://bootstrap-taghelpers.abp.io/Components/Breadcrumbs) to see it in action. |
|||
|
|||
## abp-breadcrumb-item Attributes |
|||
|
|||
- **title**: Sets the text of the breadcrumb item. |
|||
- **active**: Sets the active breadcrumb item. Last item is active by default, if no other item is active. |
|||
- **href**: A value indicates if an `abp-breadcrumb-item` has a link. Should be a string link value. |
|||
@ -0,0 +1,114 @@ |
|||
# Navs |
|||
|
|||
## Introduction |
|||
|
|||
`abp-nav` is the basic tag helper component derived from bootstrap nav element. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<abp-nav nav-style="Pill" align="Center"> |
|||
<abp-nav-item> |
|||
<a abp-nav-link active="true" href="#">Active</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link href="#">Longer nav link</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link href="#">link</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link disabled="true" href="#">disabled</a> |
|||
</abp-nav-item> |
|||
</abp-nav> |
|||
```` |
|||
|
|||
## Demo |
|||
|
|||
See the [navs demo page](https://bootstrap-taghelpers.abp.io/Components/Navs) to see it in action. |
|||
|
|||
## abp-nav Attributes |
|||
|
|||
- **nav-style**: The value indicates the positioning and style of the containing items. Should be one of the following values: |
|||
* `Default` (default value) |
|||
* `Vertical` |
|||
* `Pill` |
|||
* `PillVertical` |
|||
- **align:** The value indicates the alignment of the containing items: |
|||
* `Default` (default value) |
|||
* `Start` |
|||
* `Center` |
|||
* `End` |
|||
|
|||
### abp-nav-bar Attributes |
|||
|
|||
- **nav-style**: The value indicates the color layout of the base navigation bar. Should be one of the following values: |
|||
* `Default` (default value) |
|||
* `Dark` |
|||
* `Light` |
|||
* `Dark_Primary` |
|||
* `Dark_Secondary` |
|||
* `Dark_Success` |
|||
* `Dark_Danger` |
|||
* `Dark_Warning` |
|||
* `Dark_Info` |
|||
* `Dark_Dark` |
|||
* `Dark_Link` |
|||
* `Light_Primary` |
|||
* `Light_Secondary` |
|||
* `Light_Success` |
|||
* `Light_Danger` |
|||
* `Light_Warning` |
|||
* `Light_Info` |
|||
* `Light_Dark` |
|||
* `Light_Link` |
|||
- **size:** The value indicates size of the base navigation bar. Should be one of the following values: |
|||
* `Default` (default value) |
|||
* `Sm` |
|||
* `Md` |
|||
* `Lg` |
|||
* `Xl` |
|||
|
|||
### abp-nav-item Attributes |
|||
|
|||
**dropdown**: A value that sets the navigation item to be a dropdown menu if provided. Can be one of the following values: |
|||
|
|||
* `false` (default value) |
|||
* `true` |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
<abp-nav-bar size="Lg" navbar-style="Dark_Warning"> |
|||
<a abp-navbar-brand href="#">Navbar</a> |
|||
<abp-navbar-toggle> |
|||
<abp-navbar-nav> |
|||
<abp-nav-item active="true"> |
|||
<a abp-nav-link href="#">Home <span class="sr-only">(current)</span></a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link href="#">Link</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item dropdown="true"> |
|||
<abp-dropdown> |
|||
<abp-dropdown-button nav-link="true" text="Dropdown" /> |
|||
<abp-dropdown-menu> |
|||
<abp-dropdown-header>Dropdown header</abp-dropdown-header> |
|||
<abp-dropdown-item href="#" active="true">Action</abp-dropdown-item> |
|||
<abp-dropdown-item href="#" disabled="true">Another disabled action</abp-dropdown-item> |
|||
<abp-dropdown-item href="#">Something else here</abp-dropdown-item> |
|||
<abp-dropdown-divider /> |
|||
<abp-dropdown-item href="#">Separated link</abp-dropdown-item> |
|||
</abp-dropdown-menu> |
|||
</abp-dropdown> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link disabled="true" href="#">Disabled</a> |
|||
</abp-nav-item> |
|||
</abp-navbar-nav> |
|||
<span abp-navbar-text> |
|||
Sample Text |
|||
</span> |
|||
</abp-navbar-toggle> |
|||
</abp-nav-bar> |
|||
```` |
|||
@ -0,0 +1,61 @@ |
|||
# Tables |
|||
|
|||
## Introduction |
|||
|
|||
`abp-table` is the basic tag component for tables in abp. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<abp-table hoverable-rows="true" responsive-sm="true"> |
|||
<thead> |
|||
<tr> |
|||
<th scope="Column">#</th> |
|||
<th scope="Column">First</th> |
|||
<th scope="Column">Last</th> |
|||
<th scope="Column">Handle</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr> |
|||
<th scope="Row">1</th> |
|||
<td>Mark</td> |
|||
<td>Otto</td> |
|||
<td table-style="Danger">mdo</td> |
|||
</tr> |
|||
<tr table-style="Warning"> |
|||
<th scope="Row">2</th> |
|||
<td>Jacob</td> |
|||
<td>Thornton</td> |
|||
<td>fat</td> |
|||
</tr> |
|||
<tr> |
|||
<th scope="Row">3</th> |
|||
<td table-style="Success">Larry</td> |
|||
<td>the Bird</td> |
|||
<td>twitter</td> |
|||
</tr> |
|||
</tbody> |
|||
</abp-table> |
|||
```` |
|||
|
|||
|
|||
|
|||
## Demo |
|||
|
|||
See the [tables demo page](https://bootstrap-taghelpers.abp.io/Components/Tables) to see it in action. |
|||
|
|||
## abp-table Attributes |
|||
|
|||
- **responsive**: Used to create responsive tables up to a particular breakpoint. see [breakpoint specific](https://getbootstrap.com/docs/4.1/content/tables/#breakpoint-specific) for more information. |
|||
- **responsive-sm**: If not set to false, sets the table responsiveness for small screen devices. |
|||
- **responsive-md**: If not set to false, sets the table responsiveness for medium screen devices. |
|||
- **responsive-lg**: If not set to false, sets the table responsiveness for large screen devices. |
|||
- **responsive-xl**: If not set to false, sets the table responsiveness for extra large screen devices. |
|||
- **dark-theme**: If set to true, sets the table color theme to dark. |
|||
- **striped-rows**: If set to true, adds zebra-striping to table rows. |
|||
- **hoverable-rows**: If set to true, adds hover state to table rows. |
|||
- **border-style**: Sets the border style of the table. Should be one of the following values: |
|||
- `Default` (default) |
|||
- `Bordered` |
|||
- `Borderless` |
|||
@ -1,8 +1,8 @@ |
|||
namespace Volo.Abp.BlobStoring |
|||
{ |
|||
[BlobContainerName("Default")] |
|||
[BlobContainerName(Name)] |
|||
public class DefaultContainer |
|||
{ |
|||
|
|||
public const string Name = "Default"; |
|||
} |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
using Microsoft.AspNetCore.Mvc.Localization; |
|||
using Microsoft.AspNetCore.Mvc.Razor.Internal; |
|||
using Volo.Abp.Account.Localization; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; |
|||
|
|||
namespace Volo.Abp.Account.Web.Pages.Account |
|||
{ |
|||
public abstract class AccountPage : AbpPage |
|||
{ |
|||
[RazorInject] |
|||
public IHtmlLocalizer<AccountResource> L { get; set; } |
|||
} |
|||
} |
|||
@ -1,3 +1,2 @@ |
|||
@page "/Account/Logout" |
|||
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage |
|||
@model Volo.Abp.Account.Web.Pages.Account.LogoutModel |
|||
@model Volo.Abp.Account.Web.Pages.Account.LogoutModel |
|||
|
|||
@ -1,6 +1,8 @@ |
|||
@page |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@using Volo.Abp.Account.Web.Pages.Account |
|||
@inherits AccountPage |
|||
@model SendSecurityCodeModel |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
<h2>Send security code!</h2> |
|||
<p>TODO: This page is under construction.</p> |
|||
<p>TODO: This page is under construction.</p> |
|||
|
|||
@ -1,13 +0,0 @@ |
|||
using Microsoft.AspNetCore.Mvc.Localization; |
|||
using Microsoft.AspNetCore.Mvc.Razor.Internal; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; |
|||
using Volo.Docs.Localization; |
|||
|
|||
namespace Volo.Docs.Admin.Pages.Docs.Admin |
|||
{ |
|||
public abstract class DocsAdminPage : AbpPage |
|||
{ |
|||
[RazorInject] |
|||
public IHtmlLocalizer<DocsResource> L { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Docs.Documents |
|||
{ |
|||
public interface INavigationTreePostProcessor |
|||
{ |
|||
Task ProcessAsync(NavigationTreePostProcessorContext context); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
namespace Volo.Docs.Documents |
|||
{ |
|||
public class NavigationTreePostProcessorContext |
|||
{ |
|||
public DocumentWithDetailsDto Document { get; } |
|||
public NavigationNode RootNode { get; } |
|||
|
|||
public NavigationTreePostProcessorContext(DocumentWithDetailsDto document, NavigationNode rootNode) |
|||
{ |
|||
Document = document; |
|||
RootNode = rootNode; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Docs.Documents |
|||
{ |
|||
public sealed class NullNavigationTreePostProcessor : INavigationTreePostProcessor |
|||
{ |
|||
public static NullNavigationTreePostProcessor Instance { get; } = new NullNavigationTreePostProcessor(); |
|||
|
|||
private NullNavigationTreePostProcessor() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Task ProcessAsync(NavigationTreePostProcessorContext context) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
using Microsoft.AspNetCore.Mvc.Localization; |
|||
using Microsoft.AspNetCore.Mvc.Razor.Internal; |
|||
using ProductManagement.Localization; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; |
|||
|
|||
namespace ProductManagement.Pages.ProductManagement |
|||
{ |
|||
public abstract class ProductManagementPage : AbpPage |
|||
{ |
|||
[RazorInject] |
|||
public IHtmlLocalizer<ProductManagementResource> L { get; set; } |
|||
|
|||
public const string DefaultTitle = "ProductManagement"; |
|||
|
|||
public string GetTitle(string title = null) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(title)) |
|||
{ |
|||
return DefaultTitle; |
|||
} |
|||
|
|||
return title; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue