diff --git a/Directory.Packages.props b/Directory.Packages.props index 8f818a02ce..0c674bf741 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,6 +88,11 @@ + + + + + diff --git a/docs/en/framework/infrastructure/artificial-intelligence.md b/docs/en/framework/infrastructure/artificial-intelligence.md new file mode 100644 index 0000000000..652c7c8233 --- /dev/null +++ b/docs/en/framework/infrastructure/artificial-intelligence.md @@ -0,0 +1,307 @@ +# Artificial Intelligence + +ABP provides a simple way to integrate AI capabilities into your applications by unifying two popular .NET AI stacks under a common concept called a "workspace": + +- Microsoft.Extensions.AI `IChatClient` +- Microsoft.SemanticKernel `Kernel` + +A workspace is just a named scope. You configure providers per workspace and then resolve either default services (for the "Default" workspace) or workspace-scoped services. + +## Installation + +> This package is not included by default. Install it to enable AI features. + +It is suggested to use the ABP CLI to install the package. Open a command line window in the folder of the project (.csproj file) and type the following command: + +```bash +abp add-package Volo.Abp.AI +``` + +### Manual Installation + +Add nuget package to your project: + +```bash +dotnet add package Volo.Abp.AI +``` + +Then add the module dependency to your module class: + +```csharp +using Volo.Abp.AI; +using Volo.Abp.Modularity; + +[DependsOn(typeof(AbpAIModule))] +public class MyProjectModule : AbpModule +{ +} +``` + +## Usage + +### Chat Client + +#### Default configuration (quick start) + +Configure the default workspace to inject `IChatClient` directly. + +```csharp +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Volo.Abp.AI; +using Volo.Abp.Modularity; + +public class MyProjectModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.PreConfigure(options => + { + options.Workspaces.ConfigureDefault(configuration => + { + configuration.ConfigureChatClient(chatClientConfiguration => + { + chatClientConfiguration.Builder = new ChatClientBuilder( + sp => new OllamaApiClient("http://localhost:11434", "mistral") + ); + }); + + // Chat client only in this quick start + }); + }); + } +} +``` + +Once configured, inject the default chat client: + +```csharp +using Microsoft.Extensions.AI; + +public class MyService +{ + private readonly IChatClient _chatClient; // default chat client + + public MyService(IChatClient chatClient) + { + _chatClient = chatClient; + } +} +``` + +#### Workspace configuration + +Workspaces allow multiple, isolated AI configurations. Define workspace types (optionally decorated with `WorkspaceNameAttribute`). If omitted, the type’s full name is used. + +```csharp +using Volo.Abp.AI; + +[WorkspaceName("GreetingAssistant")] +public class GreetingAssistant // ChatClient-only workspace +{ +} +``` + +Configure a ChatClient workspace: + +```csharp +public class MyProjectModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.PreConfigure(options => + { + options.Workspaces.Configure(configuration => + { + configuration.ConfigureChatClient(chatClientConfiguration => + { + chatClientConfiguration.Builder = new ChatClientBuilder( + sp => new OllamaApiClient("http://localhost:11434", "mistral") + ); + + chatClientConfiguration.BuilderConfigurers.Add(builder => + { + // Anything you want to do with the builder: + // builder.UseFunctionInvocation().UseLogging(); // For example + }); + }); + }); + }); + } +} +``` + +### Semantic Kernel + +#### Default configuration + + +```csharp +public class MyProjectModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.PreConfigure(options => + { + options.Workspaces.ConfigureDefault(configuration => + { + configuration.ConfigureKernel(kernelConfiguration => + { + kernelConfiguration.Builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatClient("...", "..."); + }); + // Note: Chat client is not configured here + }); + }); + } +} +``` + +Once configured, inject the default kernel: + +```csharp +using System.Threading.Tasks; +using Volo.Abp.AI; + +public class MyService +{ + private readonly IKernelAccessor _kernelAccessor; + public MyService(IKernelAccessor kernelAccessor) + { + _kernelAccessor = kernelAccessor; + } + + public async Task DoSomethingAsync() + { + var kernel = _kernelAccessor.Kernel; // Kernel might be null if no workspace is configured. + + var result = await kernel.InvokeAsync(/*... */); + } +} +``` + +#### Workspace configuration + +```csharp +public class MyProjectModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.PreConfigure(options => + { + options.Workspaces.Configure(configuration => + { + configuration.ConfigureKernel(kernelConfiguration => + { + kernelConfiguration.Builder = Kernel.CreateBuilder() + .AddOpenAIChatCompletion("...", "..."); + }); + }); + }); + } +} +``` + +#### Workspace usage + +```csharp +using Microsoft.Extensions.AI; +using Volo.Abp.AI; +using Microsoft.SemanticKernel; + +public class PlanningService +{ + private readonly IKernelAccessor _kernelAccessor; + private readonly IChatClient _chatClient; // available even if only Kernel is configured + + public PlanningService( + IKernelAccessor kernelAccessor, + IChatClient chatClient) + { + _kernelAccessor = kernelAccessor; + _chatClient = chatClient; + } + + public async Task PlanAsync(string topic) + { + var kernel = _kernelAccessor.Kernel; // Microsoft.SemanticKernel.Kernel + // Use Semantic Kernel APIs if needed... + + var response = await _chatClient.GetResponseAsync( + [new ChatMessage(ChatRole.User, $"Create a content plan for: {topic}")] + ); + return response?.Message?.Text ?? string.Empty; + } +} +``` + +## Options + +`AbpAIOptions` configuration pattern offers `ConfigureChatClient(...)` and `ConfigureKernel(...)` methods for configuration. These methods are defined in the `WorkspaceConfiguration` class. They are used to configure the `ChatClient` and `Kernel` respectively. + +`Builder` is set once and is used to build the `ChatClient` or `Kernel` instance. `BuilderConfigurers` is a list of actions that are applied to the `Builder` instance for incremental changes. These actions are executed in the order they are added. + +If a workspace configures only the Kernel, a chat client may still be exposed for that workspace through the Kernel’s service provider (when available). + + +## Advanced Usage and Customizations + +### Addding Your Own DelegatingChatClient + +If you want to build your own decorator, implement a `DelegatingChatClient` derivative and provide an extension method that adds it to the `ChatClientBuilder` using `builder.Use(...)`. + +Example sketch: + +```csharp +using Microsoft.Extensions.AI; + +public class SystemMessageChatClient : DelegatingChatClient +{ + public SystemMessageChatClient(IChatClient inner, string systemMessage) : base(inner) + { + SystemMessage = systemMessage; + } + + public string SystemMessage { get; set; } + + public override Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + // Mutate messages/options as needed, then call base + return base.GetResponseAsync(messages, options, cancellationToken); + } +} + +public static class SystemMessageChatClientExtensions +{ + public static ChatClientBuilder UseSystemMessage(this ChatClientBuilder builder, string systemMessage) + { + return builder.Use(client => new SystemMessageChatClient(client, systemMessage)); + } +} +``` + + +```cs +chatClientConfiguration.BuilderConfigurers.Add(builder => +{ + builder.UseSystemMessage("You are a helpful assistant that greets users in a friendly manner with their names."); +}); +``` + +## Technical Anatomy + +- `AbpAIModule`: Wires up configured workspaces, registers keyed services and default services for the `"Default"` workspace. +- `AbpAIOptions`: Holds `Workspaces` and provides helper methods for internal keyed service naming. +- `WorkspaceConfigurationDictionary` and `WorkspaceConfiguration`: Configure per-workspace Chat Client and Kernel. +- `ChatClientConfiguration` and `KernelConfiguration`: Hold builders and a list of ordered builder configurers. +- `WorkspaceNameAttribute`: Names a workspace; falls back to the type’s full name if not specified. +- `IChatClient`: Typed chat client for a workspace. +- `IKernelAccessor`: Provides access to the workspace’s `Kernel` instance if configured. +- `AbpAIWorkspaceOptions`: Exposes `ConfiguredWorkspaceNames` for diagnostics. + +There are no database tables for this feature; it is a pure configuration and DI integration layer. + +## See Also + +- Microsoft.Extensions.AI (Chat Client) +- Microsoft Semantic Kernel \ No newline at end of file diff --git a/docs/en/framework/infrastructure/index.md b/docs/en/framework/infrastructure/index.md index 681e593aba..e5691612dc 100644 --- a/docs/en/framework/infrastructure/index.md +++ b/docs/en/framework/infrastructure/index.md @@ -3,6 +3,7 @@ ABP provides a complete infrastructure for creating real world software solutions with modern architectures based on the .NET platform. Each of the following documents explains an infrastructure feature: * [Audit Logging](./audit-logging.md) +* [Artificial Intelligence](./artificial-intelligence.md) * [Background Jobs](./background-jobs/index.md) * [Background Workers](./background-workers/index.md) * [BLOB Storing](./blob-storing/index.md) diff --git a/docs/en/framework/ui/react-native/index.md b/docs/en/framework/ui/react-native/index.md index 4b6a0b5ec7..0f39752a82 100644 --- a/docs/en/framework/ui/react-native/index.md +++ b/docs/en/framework/ui/react-native/index.md @@ -1,7 +1,7 @@ ````json //[doc-params] { - "Tiered": ["No", "Yes"] + "Architecture": ["Monolith", "Tiered", "Microservice"] } ```` @@ -20,15 +20,23 @@ Please follow the steps below to prepare your development environment for React 1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js v20.11+ installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. 2. **[Optional] Install Yarn:** You may install Yarn v1 (not v2) following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 delivers an arguably better developer experience compared to npm v6 and below. You may skip this step and work with npm, which is built-in in Node.js, instead. 3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to React Native projects. -4. **Install an Emulator:** React Native applications need an Android emulator or an iOS simulator to run on your OS. See the [Android Studio Emulator](https://docs.expo.io/workflow/android-simulator/) or [iOS Simulator](https://docs.expo.io/workflow/ios-simulator/) on expo.io documentation to learn how to set up an emulator. +4. **Install an Emulator/Simulator:** React Native applications need an Android emulator or an iOS simulator to run on your OS. If you do not have Android Studio installed and configured on your system, we recommend [setting up android emulator without android studio](setting-up-android-emulator.md). + +If you prefer the other way, you can check the [Android Studio Emulator](https://docs.expo.dev/workflow/android-studio-emulator/) or [iOS Simulator](https://docs.expo.dev/workflow/ios-simulator/) on expo.io documentation to learn how to set up an emulator. ## How to Start a New React Native Project You have multiple options to initiate a new React Native project that works with ABP: -### 1. Using ABP CLI +### 1. Using ABP Studio + +ABP Studio application is the most convenient and flexible way to initiate a React Native application based on ABP framework. You can follow the [tool documentation](../../../studio) and select the option below: + +![React Native option](../../../images/react-native-option.png) + +### 2. Using ABP CLI -ABP CLI is probably the most convenient and flexible way to initiate an ABP solution with a React Native application. Simply [install the ABP CLI](../../../cli) and run the following command in your terminal: +ABP CLI is another way of creating an ABP solution with a React Native application. Simply [install the ABP CLI](../../../cli) and run the following command in your terminal: ```shell abp new MyCompanyName.MyProjectName -csf -u -m react-native @@ -38,33 +46,209 @@ abp new MyCompanyName.MyProjectName -csf -u -m react-native This command will prepare a solution with an **Angular** or an **MVC** (depends on your choice), a **.NET Core**, and a **React Native** project in it. -### 2. Generating a CLI Command from Get Started Page - -You can generate a CLI command on the [get started page of the abp.io website](https://abp.io/get-started). Then, use the command on your terminal to create a new [Startup Template](../../../solution-templates). - ## How to Configure & Run the Backend > React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development. -> When you are using OpenIddict, You should remove 'clientSecret' on Environment.js (if exists) and disable "HTTPS-only" settings. (Openiddict has default since Version 6.0) - -A React Native application running on an Android emulator or a physical phone **can not connect to the backend** on `localhost`. To fix this problem, it is necessary to run the backend application on your **local IP address**. - -{{ if Tiered == "No"}} -![React Native host project local IP entry](../../../images/rn-host-local-ip.png) - -- Open the `appsettings.json` file in the `.HttpApi.Host` folder. Replace the `localhost` address on the `SelfUrl` and `Authority` properties with your local IP address. -- Open the `launchSettings.json` file in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. - -{{ else if Tiered == "Yes" }} - -![React Native tiered project local IP entry](../../../images/rn-tiered-local-ip.png) - -- Open the `appsettings.json` file in the `.AuthServer` folder. Replace the `localhost` address on the `SelfUrl` property with your local IP address. -- Open the `launchSettings.json` file in the `.AuthServer/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. -- Open the `appsettings.json` file in the `.HttpApi.Host` folder. Replace the `localhost` address on the `Authority` property with your local IP address. -- Open the `launchSettings.json` file in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. - +A React Native application running on an Android emulator or a physical phone **can not connect to the backend** on `localhost`. To fix this problem, it is necessary to run the backend application using the `Kestrel` configuration. + +{{ if Architecture == "Monolith" }} + +![React Native monolith host project configuration](../../../images/react-native-monolith-be-config.png) + +- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address on the `RootUrl` property with your local IP address. Then, run the database migrator. +- Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests just to test the react native application on the development environment. + + ```json + { + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:44323" //replace with your host port + } + } + } + } + ``` + +{{ else if Architecture == "Tiered" }} + +![React Native tiered project configuration](../../../images/react-native-tiered-be-config.png) + +- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address on the `RootUrl` property with your local IP address. Then, run the database migrator. +- Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests just to test the react native application on the development environment. + + ```json + { + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:44337" + } + } + } + } + ``` + +- Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests again. In addition, you will need to introduce the authentication server as mentioned above. + + ```json + { + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:44389" //replace with your host port + } + } + }, + "AuthServer": { + "Authority": "http://192.168.1.37:44337/", //replace with your IP and authentication port + "MetaAddress": "http://192.168.1.37:44337/", + "RequireHttpsMetadata": false, + "Audience": "MyTieredProject" //replace with your application name + } + } + ``` + +{{ else if Architecture == "Microservice" }} + +![React Native microservice project configuration](../../../images/react-native-microservice-be-config.png) + +- Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests just to test the react native application on the development environment. + + ```json + { + "App": { + "EnablePII": true + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:44319" + } + } + } + } + ``` + +- Open the `appsettings.Development.json` file in the `.AdministrationService` folder. Add this configuration to accept global requests just to test the react native application on the development environment. You should also provide the authentication server configuration. In addition, you need to apply the same process for all the services you would use in the react native application. + + ```json + { + "App": { + "EnablePII": true + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:44357" + } + } + }, + "AuthServer": { + "Authority": "http://192.168.1.36:44319/", + "MetaAddress": "http://192.168.1.36:44319/", + "RequireHttpsMetadata": false, + "Audience": "AdministrationService" + } + } + ``` + +- Update the `appsettings.json` file in the `.IdentityService` folder. Replace the `localhost` configuration with your local IP address for the react native application + + ```json + { + //... + "OpenIddict": { + "Applications": { + //... + "ReactNative": { + "RootUrl": "exp://192.168.1.36:19000" + }, + "MobileGateway": { + "RootUrl": "http://192.168.1.36:44347/" + }, + //... + }, + //... + } + } + ``` + +- Lastly, update the mobile gateway configurations as following: + + ```json + //gateways/mobile/MyMicroserviceProject.MobileGateway/Properties/launchSettings.json + { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://192.168.1.36:44347" //update with your IP address + } + }, + "profiles": { + //... + "MyMicroserviceProject.MobileGateway": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "http://192.168.1.36:44347", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } + ``` + + ```json + //gateways/mobile/MyMicroserviceProject.MobileGateway/appsettings.json + { + //Update clusters with your IP address + //... + "ReverseProxy": { + //... + "Clusters": { + "AuthServer": { + "Destinations": { + "AuthServer": { + "Address": "http://192.168.1.36:44319/" + } + } + }, + "Administration": { + "Destinations": { + "Administration": { + "Address": "http://192.168.1.36:44357/" + } + } + }, + "Saas": { + "Destinations": { + "Saas": { + "Address": "http://192.168.1.36:44330/" + } + } + }, + "Identity": { + "Destinations": { + "Identity": { + "Address": "http://192.168.1.36:44397/" + } + } + }, + "Language": { + "Destinations": { + "Identity": { + "Address": "http://192.168.1.36:44310/" + } + } + } + } + } + } + ``` {{ end }} Run the backend application as described in the [getting started document](../../../get-started). @@ -73,7 +257,7 @@ Run the backend application as described in the [getting started document](../.. ## How to disable the Https-only settings of OpenIddict -Open the {{ if Tiered == "No" }}`MyProjectNameHttpApiHostModule`{{ else if Tiered == "Yes" }}`MyProjectNameAuthServerModule`{{ end }} project and copy-paste the below code-block to the `PreConfigureServices` method: +Open the {{ if Architecture == "Monolith" }}`MyProjectNameHttpApiHostModule`{{ if Architecture == "Tiered" }}`MyProjectNameAuthServerModule`{{ end }} project and copy-paste the below code-block to the `PreConfigureServices` method: ```csharp #if DEBUG @@ -89,21 +273,27 @@ Open the {{ if Tiered == "No" }}`MyProjectNameHttpApiHostModule`{{ else if Tiere 1. Make sure the [database migration is complete](../../../get-started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](../../../get-started?UI=NG&DB=EF&Tiered=No#run-the-application). 2. Open `react-native` folder and run `yarn` or `npm install` if you have not already. -3. Open the `Environment.js` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: +3. Open the `Environment.ts` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: + +{{ if Architecture == "Monolith" }} -![react native environment local IP](../../../images/rn-environment-local-ip.png) +![react native monolith environment local IP](../../../images/react-native-monolith-environment-local-ip.png) -{{ if Tiered == "Yes" }} +{{ else if Architecture == "Tiered" }} + +![react native tiered environment local IP](../../../images/react-native-tiered-environment-local-ip.png) > Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` project. -{{else}} +{{ else }} + +![react native microservice environment local IP](../../../images/react-native-microservice-environment-local-ip.png) -> Make sure that `issuer` and `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` project. +> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.AuthServer` project. {{ end }} -4. Run `yarn start` or `npm start`. Wait for the Expo CLI to print the opitons. +1. Run `yarn start` or `npm start`. Wait for the Expo CLI to print the opitons. > The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features. @@ -113,14 +303,14 @@ In the above image, you can start the application with an Android emulator, an i ### Expo -![React Native login screen on iPhone 11](../../../images/rn-login-iphone.png) +![React Native login screen on iPhone 16](../../../images/rn-login-iphone.png) ### Android Studio 1. Start the emulator in **Android Studio** before running the `yarn start` or `npm start` command. 2. Press **a** to open in Android Studio. -![React Native login screen on iPhone 11](../../../images/rn-login-android-studio.png) +![React Native login screen on Android Device](../../../images/rn-login-android-studio.png) Enter **admin** as the username and **1q2w3E\*** as the password to login to the application. diff --git a/docs/en/framework/ui/react-native/setting-up-android-emulator.md b/docs/en/framework/ui/react-native/setting-up-android-emulator.md new file mode 100644 index 0000000000..8ead542a76 --- /dev/null +++ b/docs/en/framework/ui/react-native/setting-up-android-emulator.md @@ -0,0 +1,127 @@ +# Setting Up Android Emulator Without Android Studio (Windows, macOS, Linux) + +This guide explains how to install and run an Android emulator **without Android Studio**, using only **Command Line Tools**. + +--- + +## 1. Download Required Tools + +Go to: [https://developer.android.com/studio#command-tools](https://developer.android.com/studio#command-tools) +Download the "Command line tools only" package for your OS: + +- **Windows:** `commandlinetools-win-*.zip` +- **macOS:** `commandlinetools-mac-*.zip` +- **Linux:** `commandlinetools-linux-*.zip` + +--- + +## 2. Create the Required Directory Structure + +### Windows: +``` +C:\Android\ +└── cmdline-tools\ + └── latest\ + └── [extract all files from the zip here] +``` + +### macOS / Linux: +``` +~/Android/ +└── cmdline-tools/ + └── latest/ + └── [extract all files from the zip here] +``` + +> You need to create the `latest` folder manually. + +--- + +## 3. Set Environment Variables + +### Windows (temporary for CMD session): +```cmd +set PATH=C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator;%PATH% +``` + +### macOS / Linux: +Add the following to your `.bashrc`, `.zshrc`, or `.bash_profile` file: + +```bash +export ANDROID_HOME=$HOME/Android +export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin +export PATH=$PATH:$ANDROID_HOME/platform-tools +export PATH=$PATH:$ANDROID_HOME/emulator +``` + +> Apply the changes: +```bash +source ~/.zshrc # or ~/.bashrc if you're using bash +``` + +--- + +## 4. Install SDK Components + +Install platform tools, emulator, and a system image: + +```bash +sdkmanager --sdk_root=$ANDROID_HOME "platform-tools" "platforms;android-34" "system-images;android-34;google_apis;x86_64" "emulator" +``` + +> On Windows, replace `$ANDROID_HOME` with `--sdk_root=C:\Android`. + +--- + +## 5. Create an AVD (Android Virtual Device) + +### List available devices: +```bash +avdmanager list devices +``` + +### Create your AVD: +```bash +avdmanager create avd -n myEmu -k "system-images;android-34;google_apis;x86_64" --device "pixel" +``` + +--- + +## 6. Start the Emulator + +```bash +emulator -avd myEmu +``` + +The emulator window should open + +--- + +## Extra Tools and Commands + +### List connected devices with ADB: +```bash +adb devices +``` + +### Install an APK: +```bash +adb install myApp.apk +``` + +--- + +## Troubleshooting + +| Problem | Explanation | +|--------|-------------| +| `sdkmanager not found` | Make sure `PATH` includes the `latest/bin` directory | +| `x86_64 system image not found` | Make sure you've downloaded it using `sdkmanager` | +| `emulator not found` | Add the `emulator` directory to `PATH` | +| `setx` truncates path (Windows) | Use GUI to update environment variables manually | + +--- + +## Summary + +You can now run an Android emulator without installing Android Studio, entirely through the command line. This emulator can be used for React Native or any mobile development framework. diff --git a/docs/en/get-started/images/abp-studio-mobile-sample.gif b/docs/en/get-started/images/abp-studio-mobile-sample.gif index 654a92eef9..4301c451b6 100644 Binary files a/docs/en/get-started/images/abp-studio-mobile-sample.gif and b/docs/en/get-started/images/abp-studio-mobile-sample.gif differ diff --git a/docs/en/get-started/layered-web-application.md b/docs/en/get-started/layered-web-application.md index 3dcbb90f75..c1e08fdb93 100644 --- a/docs/en/get-started/layered-web-application.md +++ b/docs/en/get-started/layered-web-application.md @@ -242,6 +242,8 @@ You can use `admin` as username and `1q2w3E*` as default password to login to th > Note: If you haven't selected a mobile framework, you can skip this step. +Before starting the mobile application, ensure that you have configured it for [react-native](../framework/ui/react-native) or [MAUI](../framework/ui/maui). + You can start the following application(s): {{ if Tiered == "Yes" }} @@ -255,8 +257,7 @@ You can start the following application(s): {{ else }} - `Acme.BookStore.Web` {{ end }} - -Before starting the mobile application, ensure that you configure it for [react-native](../framework/ui/react-native) or [MAUI](../framework/ui/maui). +- `react-native` or `Acme.Bookstore.Maui` ![mobile-sample](images/abp-studio-mobile-sample.gif) diff --git a/docs/en/images/author-input-in-book-form.png b/docs/en/images/author-input-in-book-form.png index f20254cce7..5204a234e6 100644 Binary files a/docs/en/images/author-input-in-book-form.png and b/docs/en/images/author-input-in-book-form.png differ diff --git a/docs/en/images/author-list-with-options.png b/docs/en/images/author-list-with-options.png index 4066938914..c6c98e8f5e 100644 Binary files a/docs/en/images/author-list-with-options.png and b/docs/en/images/author-list-with-options.png differ diff --git a/docs/en/images/author-list.png b/docs/en/images/author-list.png index b6a73d8f8d..59d5ad689e 100644 Binary files a/docs/en/images/author-list.png and b/docs/en/images/author-list.png differ diff --git a/docs/en/images/authors-in-book-form.png b/docs/en/images/authors-in-book-form.png index 2e0a13a22c..260abddefa 100644 Binary files a/docs/en/images/authors-in-book-form.png and b/docs/en/images/authors-in-book-form.png differ diff --git a/docs/en/images/book-list-with-author.png b/docs/en/images/book-list-with-author.png index c5ab6541e8..6cf46aa2e9 100644 Binary files a/docs/en/images/book-list-with-author.png and b/docs/en/images/book-list-with-author.png differ diff --git a/docs/en/images/book-list-with-options.png b/docs/en/images/book-list-with-options.png index a6bf87e71f..e327ae3bf5 100644 Binary files a/docs/en/images/book-list-with-options.png and b/docs/en/images/book-list-with-options.png differ diff --git a/docs/en/images/book-list.png b/docs/en/images/book-list.png index f14ac93a64..0cd0b74686 100644 Binary files a/docs/en/images/book-list.png and b/docs/en/images/book-list.png differ diff --git a/docs/en/images/book-store-menu-item.png b/docs/en/images/book-store-menu-item.png index f190f176ce..cc7b438d74 100644 Binary files a/docs/en/images/book-store-menu-item.png and b/docs/en/images/book-store-menu-item.png differ diff --git a/docs/en/images/books-menu-item.png b/docs/en/images/books-menu-item.png index eb9c1042fd..bc80353bc0 100644 Binary files a/docs/en/images/books-menu-item.png and b/docs/en/images/books-menu-item.png differ diff --git a/docs/en/images/create-author.png b/docs/en/images/create-author.png index 69e36bc50d..746c916473 100644 Binary files a/docs/en/images/create-author.png and b/docs/en/images/create-author.png differ diff --git a/docs/en/images/create-book-button-visibility.png b/docs/en/images/create-book-button-visibility.png index 1b153f9f65..e7f6021175 100644 Binary files a/docs/en/images/create-book-button-visibility.png and b/docs/en/images/create-book-button-visibility.png differ diff --git a/docs/en/images/create-book-icon.png b/docs/en/images/create-book-icon.png index 4d4be5ef30..e24f2f53cf 100644 Binary files a/docs/en/images/create-book-icon.png and b/docs/en/images/create-book-icon.png differ diff --git a/docs/en/images/create-book.png b/docs/en/images/create-book.png index d7a9675c72..c931c60a0d 100644 Binary files a/docs/en/images/create-book.png and b/docs/en/images/create-book.png differ diff --git a/docs/en/images/delete-author-alert.png b/docs/en/images/delete-author-alert.png index 41537d1756..ce1992478d 100644 Binary files a/docs/en/images/delete-author-alert.png and b/docs/en/images/delete-author-alert.png differ diff --git a/docs/en/images/delete-book-alert.png b/docs/en/images/delete-book-alert.png index 1676c37ce8..4ef12f3896 100644 Binary files a/docs/en/images/delete-book-alert.png and b/docs/en/images/delete-book-alert.png differ diff --git a/docs/en/images/delete-book.png b/docs/en/images/delete-book.png index f6f554d6af..cdfddce673 100644 Binary files a/docs/en/images/delete-book.png and b/docs/en/images/delete-book.png differ diff --git a/docs/en/images/react-native-environment-local-ip.png b/docs/en/images/react-native-environment-local-ip.png new file mode 100644 index 0000000000..9887fbcd16 Binary files /dev/null and b/docs/en/images/react-native-environment-local-ip.png differ diff --git a/docs/en/images/react-native-introduction.gif b/docs/en/images/react-native-introduction.gif index 15963556aa..4c89b56354 100644 Binary files a/docs/en/images/react-native-introduction.gif and b/docs/en/images/react-native-introduction.gif differ diff --git a/docs/en/images/react-native-microservice-be-config.png b/docs/en/images/react-native-microservice-be-config.png new file mode 100644 index 0000000000..2be862d04b Binary files /dev/null and b/docs/en/images/react-native-microservice-be-config.png differ diff --git a/docs/en/images/react-native-monolith-be-config.png b/docs/en/images/react-native-monolith-be-config.png new file mode 100644 index 0000000000..08166473d3 Binary files /dev/null and b/docs/en/images/react-native-monolith-be-config.png differ diff --git a/docs/en/images/react-native-monolith-environment-local-ip.png b/docs/en/images/react-native-monolith-environment-local-ip.png new file mode 100644 index 0000000000..09e79419d6 Binary files /dev/null and b/docs/en/images/react-native-monolith-environment-local-ip.png differ diff --git a/docs/en/images/react-native-option.png b/docs/en/images/react-native-option.png new file mode 100644 index 0000000000..0121daa39f Binary files /dev/null and b/docs/en/images/react-native-option.png differ diff --git a/docs/en/images/react-native-store-folder.png b/docs/en/images/react-native-store-folder.png index 2567d41477..0bc3fb8134 100644 Binary files a/docs/en/images/react-native-store-folder.png and b/docs/en/images/react-native-store-folder.png differ diff --git a/docs/en/images/react-native-tiered-be-config.png b/docs/en/images/react-native-tiered-be-config.png new file mode 100644 index 0000000000..f59fac5164 Binary files /dev/null and b/docs/en/images/react-native-tiered-be-config.png differ diff --git a/docs/en/images/react-native-tiered-environment-local-ip.png b/docs/en/images/react-native-tiered-environment-local-ip.png new file mode 100644 index 0000000000..604cb26115 Binary files /dev/null and b/docs/en/images/react-native-tiered-environment-local-ip.png differ diff --git a/docs/en/images/rn-login-android-studio.png b/docs/en/images/rn-login-android-studio.png index 4386e95268..e99b835ce0 100644 Binary files a/docs/en/images/rn-login-android-studio.png and b/docs/en/images/rn-login-android-studio.png differ diff --git a/docs/en/images/rn-login-iphone.png b/docs/en/images/rn-login-iphone.png index 69df4d9d55..576c260f5e 100644 Binary files a/docs/en/images/rn-login-iphone.png and b/docs/en/images/rn-login-iphone.png differ diff --git a/docs/en/images/update-author.png b/docs/en/images/update-author.png index c332994811..b3f7c92074 100644 Binary files a/docs/en/images/update-author.png and b/docs/en/images/update-author.png differ diff --git a/docs/en/images/update-book.png b/docs/en/images/update-book.png index c746550939..6b0dd851c9 100644 Binary files a/docs/en/images/update-book.png and b/docs/en/images/update-book.png differ diff --git a/docs/en/images/update-delete-book-button-visibility.png b/docs/en/images/update-delete-book-button-visibility.png index fc9f69e870..70e6506894 100644 Binary files a/docs/en/images/update-delete-book-button-visibility.png and b/docs/en/images/update-delete-book-button-visibility.png differ diff --git a/docs/en/solution-templates/layered-web-application/mobile-applications.md b/docs/en/solution-templates/layered-web-application/mobile-applications.md index 09da244f27..4a6b6d057a 100644 --- a/docs/en/solution-templates/layered-web-application/mobile-applications.md +++ b/docs/en/solution-templates/layered-web-application/mobile-applications.md @@ -94,22 +94,22 @@ You can follow [Mobile Application Development Tutorial - MAUI](../../tutorials/ This is the mobile application that is built based on Facebook's [React Native framework](https://reactnative.dev/) and [Expo](https://expo.dev/). It will be in the solution only if you've selected React Native as your mobile application option. #### Project Structure -- **Environment.js**: file using for provide application level variables like `apiUrl`, `oAuthConfig` and etc. +- **Environment.ts**: file using for provide application level variables like `apiUrl`, `oAuthConfig` and etc. - **api**: The `api` folder contains HTTP request files that simplify API management in the React Native starter template - - `API.js:` exports **axiosInstance**. It provides axios instance filled api url + - `API.ts:` exports **axiosInstance**. It provides axios instance filled api url - **components**: In the `components` folder you can reach built in react native components that you can use in your app. These components **facilitates** your list, select and etc. operations - **contexts**: `contexts` folder contains [react context](https://react.dev/reference/react/createContext). You can expots your contexts in this folder. `Localization context provided in here` -- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After create new *FeatureName*Navigator we need to provide in `DrawerNavigator.js` file as `Drawer.Screen` +- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After create new *FeatureName*Navigator we need to provide in `DrawerNavigator.tsx` file as `Drawer.Screen` - **screens**: is the content of navigated page. We'll pass as component property to [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/) -- **store**: folder manages state-management operations. We will define `actions`, `reducers`, `sagas` and `selectors` here. +- **store**: folder manages state-management operations. We will define `actions`, `listeners`, `reducers`, and `selectors` here. -- **styles**: folder contains app styles. `system-style.js` comes built in template we can also add new styles. +- **styles**: folder contains app styles. `system-style.ts` comes built in template we can also add new styles. - **utils**: folder contains helper functions that we can use in application diff --git a/docs/en/solution-templates/microservice/mobile-applications.md b/docs/en/solution-templates/microservice/mobile-applications.md index e28afbc3c8..d3e8fe19d0 100644 --- a/docs/en/solution-templates/microservice/mobile-applications.md +++ b/docs/en/solution-templates/microservice/mobile-applications.md @@ -139,22 +139,28 @@ You can follow [Mobile Application Development Tutorial - MAUI](../../tutorials/ This is the mobile application that is built based on Facebook's [React Native framework](https://reactnative.dev/) and [Expo](https://expo.dev/). It will be in the solution only if you've selected React Native as your mobile application option. #### Project Structure -- **Environment.js**: file using for provide application level variables like `apiUrl`, `oAuthConfig` and etc. +- **Environment.ts**: file using for providing application level variables like `apiUrl`, `oAuthConfig` and etc. - **api**: The `api` folder contains HTTP request files that simplify API management in the React Native starter template - - `API.js:` exports **axiosInstance**. It provides axios instance filled api url + - `API.ts:` exports **axiosInstance**. It provides axios instance filled api url. -- **components**: In the `components` folder you can reach built in react native components that you can use in your app. These components **facilitates** your list, select and etc. operations +- **components**: In the `components` folder, you can reach built in react native components that you can use in your app. These components **facilitates** your list, select and etc. operations. - **contexts**: `contexts` folder contains [react context](https://react.dev/reference/react/createContext). You can expots your contexts in this folder. `Localization context provided in here` -- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After create new *FeatureName*Navigator we need to provide in `DrawerNavigator.js` file as `Drawer.Screen` +- **hocs**: this folder is added to contain higher order components. The purpose is to wrap components with additional features or properties. It initially has a `PermissionHoc.tsx` that wraps a component to check the permission grant status. -- **screens**: is the content of navigated page. We'll pass as component property to [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/) +- **hooks**: covers the react native hooks where you can get a reference from [the official documentation](https://react.dev/reference/react/hooks). -- **store**: folder manages state-management operations. We will define `actions`, `reducers`, `sagas` and `selectors` here. +- **interceptors**: initializes a file called `APIInterceptor.ts` that has a function to manage the http operations in a better way. -- **styles**: folder contains app styles. `system-style.js` comes built in template we can also add new styles. +- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After creating a new *FeatureName*Navigator we need to provide in `DrawerNavigator.ts` file as `Drawer.Screen` + +- **screens**: folder has the content of navigated page. We will pass as component property to [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/) + +- **store**: folder manages state-management operations. We will define `actions`, `listeners`, `reducers`, and `selectors` here. + +- **styles**: folder contains app styles. `system-style.ts` comes built in template we can also add new styles. - **utils**: folder contains helper functions that we can use in application @@ -162,6 +168,6 @@ This is the mobile application that is built based on Facebook's [React Native f React Native applications can't be run with the solution runner. You need to run them with the React Native CLI. You can check the [React Native documentation](https://reactnative.dev/docs/environment-setup) to learn how to setup the environment for React Native development. -Before running the React Native application, rest of the applications in the solution must be running. Such as AuthServer, MobileGateway and the microservices. +Before running the React Native application, the rest of the applications in the solution must be running. Such as AuthServer, MobileGateway and the microservices. Then you can run the React Native application by following this documentation: [Getting Started with the React Native](../../framework/ui/react-native/index.md). \ No newline at end of file diff --git a/docs/en/suite/solution-structure.md b/docs/en/suite/solution-structure.md index 5abd50d751..ad6a2d69e6 100644 --- a/docs/en/suite/solution-structure.md +++ b/docs/en/suite/solution-structure.md @@ -318,8 +318,8 @@ React Native application folder structure is like below: ![react-native-folder-structure](../images/react-native-folder-structure.png) -* `App.js` is the bootstrap component of the application. -* `Environment.js` file has the essential configuration of the application. `prod` and `dev` configurations are defined in this file. +* `App.tsx` is the bootstrap component of the application. +* `Environment.ts` file has the essential configuration of the application. `prod` and `dev` configurations are defined in this file. * [Contexts](https://reactjs.org/docs/context.html) are created in the `src/contexts` folder. * [Higher order components](https://reactjs.org/docs/higher-order-components.html) are created in the `src/hocs` folder. * [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) are created in the `src/hooks`. @@ -353,12 +353,11 @@ Actions, reducers, sagas, selectors are created in the `src/store` folder. Store * [**Store**](https://redux.js.org/basics/store) is defined in the `src/store/index.js` file. * [**Actions**](https://redux.js.org/basics/actions/) are payloads of information that send data from your application to your store. * [**Reducers**](https://redux.js.org/basics/reducers) specify how the application's state changes in response to actions sent to the store. -* [**Redux-Saga**](https://redux-saga.js.org/) is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage. Sagas are created in the `src/store/sagas` folder. * [**Reselect**](https://github.com/reduxjs/reselect) library is used to create memoized selectors. Selectors are created in the `src/store/selectors` folder. ### APIs -[Axios](https://github.com/axios/axios) is used as the HTTP client library. An Axios instance is exported from `src/api/API.js` file to make HTTP calls with the same config. `src/api` folder also has the API files that have been created for API calls. +[Axios](https://github.com/axios/axios) is used as the HTTP client library. An Axios instance is exported from `src/api/API.ts` file to make HTTP calls with the same config. `src/api` folder also has the API files that have been created for API calls. ### Theming @@ -381,7 +380,6 @@ See the [Testing Overview](https://reactjs.org/docs/testing.html) document. * [Axios](https://github.com/axios/axios) is used as HTTP client library. * [Redux](https://redux.js.org/) is used as state management library. * [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development. -* [Redux-Saga](https://redux-saga.js.org/) is used to manage asynchronous processes. * [Redux Persist](https://github.com/rt2zz/redux-persist) is used for state persistance. * [Reselect](https://github.com/reduxjs/reselect) is used to create memoized selectors. * [i18n-js](https://github.com/fnando/i18n-js) is used as i18n library. diff --git a/docs/en/tutorials/mobile/react-native/index.md b/docs/en/tutorials/mobile/react-native/index.md index 2dfa3df957..76b4e59983 100644 --- a/docs/en/tutorials/mobile/react-native/index.md +++ b/docs/en/tutorials/mobile/react-native/index.md @@ -6,7 +6,7 @@ React Native mobile option is *available for* ***Team*** *or higher licenses*. T > You must have an ABP Team or a higher license to be able to create a mobile application. -- This tutorial assumes that you have completed the [Web Application Development tutorial](../../book-store/part-01.md) and built an ABP based application named `Acme.BookStore` with [React Native](../../../framework/ui/react-native) as the mobile option.. Therefore, if you haven't completed the [Web Application Development tutorial](../../book-store/part-01.md), you either need to complete it or download the source code from down below and follow this tutorial. +- This tutorial assumes that you have completed the [Web Application Development tutorial](../../book-store/part-01.md) and built an ABP based application named `Acme.BookStore` with [React Native](../../../framework/ui/react-native) as the mobile option. Therefore, if you haven't completed the [Web Application Development tutorial](../../book-store/part-01.md), you either need to complete it or download the source code from down below and follow this tutorial. - In this tutorial, we will only focus on the UI side of the `Acme.BookStore` application and will implement the CRUD operations. - Before starting, please make sure that the [React Native Development Environment](../../../framework/ui/react-native/index.md) is ready on your machine. @@ -20,33 +20,31 @@ You can use the following link to download the source code of the application de ## The Book List Page -In react native there is no dynamic proxy generation, that's why we need to create the BookAPI proxy manually under the `./src/api` folder. +There is no dynamic proxy generation for the react native application, that is why we need to create the BookAPI proxy manually under the `./src/api` folder. -```js -import api from "./API"; +```ts +//./src/api/BookAPI.ts +import api from './API'; -export const getList = () => api.get("/api/app/book").then(({ data }) => data); +export const getList = () => api.get('/api/app/book').then(({ data }) => data); -export const get = (id) => - api.get(`/api/app/book/${id}`).then(({ data }) => data); +export const get = id => api.get(`/api/app/book/${id}`).then(({ data }) => data); -export const create = (input) => - api.post("/api/app/book", input).then(({ data }) => data); +export const create = input => api.post('/api/app/book', input).then(({ data }) => data); -export const update = (input, id) => - api.put(`/api/app/book/${id}`, input).then(({ data }) => data); +export const update = (input, id) => api.put(`/api/app/book/${id}`, input).then(({ data }) => data); + +export const remove = id => api.delete(`/api/app/book/${id}`).then(({ data }) => data); -export const remove = (id) => - api.delete(`/api/app/book/${id}`).then(({ data }) => data); ``` ### Add the `Book Store` menu item to the navigation -For the create menu item, navigate to `./src/navigators/DrawerNavigator.js` file and add `BookStoreStack` to `Drawer.Navigator` component. +For createing a menu item, navigate to `./src/navigators/DrawerNavigator.tsx` file and add `BookStoreStack` to `Drawer.Navigator` component. -```js +```tsx //Other imports.. -import BookStoreStackNavigator from "./BookStoreNavigator"; +import BookStoreStackNavigator from './BookStoreNavigator'; const Drawer = createDrawerNavigator(); @@ -69,41 +67,61 @@ export default function DrawerNavigator() { } ``` -Create the `BookStoreStackNavigator` in `./src/navigators/BookStoreNavigator.js`, this navigator will be used for the BookStore menu item. +Create the `BookStoreStackNavigator` inside `./src/navigators/BookStoreNavigator.tsx`, this navigator will be used for the BookStore menu item. -```js -import React from "react"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import i18n from "i18n-js"; -import HamburgerIcon from "../components/HamburgerIcon/HamburgerIcon"; -import BookStoreScreen from "../screens/Books/BookStoreScreen"; +```tsx +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { Button } from 'react-native-paper'; +import i18n from 'i18n-js'; + +import { BookStoreScreen, CreateUpdateAuthorScreen, CreateUpdateBookScreen } from '../screens'; + +import { HamburgerIcon } from '../components'; +import { useThemeColors } from '../hooks'; const Stack = createNativeStackNavigator(); export default function BookStoreStackNavigator() { + const { background, onBackground } = useThemeColors(); + return ( - - - ({ - title: i18n.t("BookStore::Menu:BookStore"), - headerLeft: () => , - })} - /> - - + + ({ + title: i18n.t('BookStore::Menu:BookStore'), + headerLeft: () => , + headerStyle: { backgroundColor: background }, + headerTintColor: onBackground, + headerShadowVisible: false, + })} + /> + ({ + title: i18n.t(route.params?.bookId ? 'BookStore::Edit' : 'BookStore::NewBook'), + headerRight: () => ( + + ), + headerStyle: { backgroundColor: background }, + headerTintColor: onBackground, + headerShadowVisible: false, + })} + /> + ); } ``` - BookStoreScreen will be used to store the `books` and `authors` page -Add the `BookStoreStack` to the screens object in the `./src/components/DrawerContent/DrawerContent.js` file. The DrawerContent component will be used to render the menu items. +Add the `BookStoreStack` to the screens object in the `./src/components/DrawerContent/DrawerContent.tsx` file. The DrawerContent component will be used to render the menu items. -```js +```tsx // Imports.. const screens = { HomeStack: { label: "::Menu:Home", iconName: "home" }, @@ -141,15 +159,18 @@ const screens = { ### Create Book List page -Before creating the book list page, we need to create the `BookStoreScreen.js` file under the `./src/screens/BookStore` folder. This file will be used to store the `books` and `authors` page. +Before creating the book list page, we need to create the `BookStoreScreen.tsx` file under the `./src/screens/BookStore` folder. This file will be used to store the `books` and `authors` page. -```js -import React from "react"; -import i18n from "i18n-js"; -import { BottomNavigation } from "react-native-paper"; -import BooksScreen from "./Books/BooksScreen"; +```tsx +import { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import i18n from 'i18n-js'; +import { BottomNavigation } from 'react-native-paper'; + +import { BooksScreen } from '../../screens'; +import { useThemeColors } from '../../hooks'; -const BooksRoute = () => ; +const BooksRoute = nav => ; function BookStoreScreen({ navigation }) { const [index, setIndex] = React.useState(0); @@ -177,24 +198,24 @@ function BookStoreScreen({ navigation }) { export default BookStoreScreen; ``` -Create the `BooksScreen.js` file under the `./src/screens/BookStore/Books` folder. +Create the `BooksScreen.tsx` file under the `./src/screens/BookStore/Books` folder. -```js -import React from "react"; +```tsx import { useSelector } from "react-redux"; import { View } from "react-native"; -import { useTheme, List } from "react-native-paper"; +import { List } from "react-native-paper"; import { getBooks } from "../../api/BookAPI"; import i18n from "i18n-js"; import DataList from "../../components/DataList/DataList"; import { createAppConfigSelector } from "../../store/selectors/AppSelectors"; +import { useThemeColors } from '../../../hooks'; function BooksScreen({ navigation }) { - const theme = useTheme(); + const { background, primary } = useThemeColors(); const currentUser = useSelector(createAppConfigSelector())?.currentUser; return ( - + {currentUser?.isAuthenticated && ( - - {/*Other screens*/} - - {/* Added this screen */} - ({ - title: i18n.t( - route.params?.bookId ? "BookStore::Edit" : "BookStore::NewBook" - ), - headerRight: () => ( - - ), - })} - /> - - + + {/*Other screens*/} + {/* Added this screen */} + ({ + title: i18n.t( + route.params?.bookId ? "BookStore::Edit" : "BookStore::NewBook" + ), + headerRight: () => ( + + ), + headerStyle: { backgroundColor: background }, + headerTintColor: onBackground, + headerShadowVisible: false, + })} + /> + ); } ``` -To navigate to the `CreateUpdateBookScreen`, we need to add the `CreateUpdateBook` button to the `BooksScreen.js` file. +To navigate to the `CreateUpdateBookScreen`, we need to add the `CreateUpdateBook` button to the `BooksScreen.tsx` file. -```js +```tsx //Other imports.. import { @@ -294,7 +315,7 @@ function BooksScreen({ navigation }) { //Other codes.. return ( - + {/* Other codes..*/} {/* Included Code */} @@ -308,7 +329,7 @@ function BooksScreen({ navigation }) { visible={true} animateFrom={"right"} iconMode={"static"} - style={[styles.fabStyle, { backgroundColor: theme.colors.primary }]} + style={[styles.fabStyle, { backgroundColor: primary }]} /> )} {/* Included Code */} @@ -332,11 +353,10 @@ const styles = StyleSheet.create({ export default BooksScreen; ``` -After adding the `CreateUpdateBook` button, we need to add the `CreateUpdateBookScreen.js` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder. +After adding the `CreateUpdateBook` button, we need to add the `CreateUpdateBookScreen.tsx` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder. -```js +```tsx import PropTypes from "prop-types"; -import React from "react"; import { create } from "../../../../api/BookAPI"; import LoadingActions from "../../../../store/actions/LoadingActions"; @@ -371,31 +391,24 @@ export default connectToRedux({ }); ``` -- In this page we'll store logic, send post/put requests, get the selected book data and etc. +- In this page we will store logic, send post/put requests, get the selected book data and etc. - This page will wrap the `CreateUpdateBookFrom` component and pass the submit function with other properties. -Create a `CreateUpdateBookForm.js` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder and add the following code to it. +Create a `CreateUpdateBookForm.tsx` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder and add the following code to it. -```js -import React, { useRef, useState } from "react"; -import { - Platform, - KeyboardAvoidingView, - StyleSheet, - View, - ScrollView, -} from "react-native"; +```tsx +import * as Yup from 'yup'; +import { useRef, useState } from 'react'; +import { Platform, KeyboardAvoidingView, StyleSheet, View, ScrollView } from 'react-native'; +import { useFormik } from 'formik'; +import i18n from 'i18n-js'; +import PropTypes from 'prop-types'; +import { TextInput, Portal, Modal, Text, Divider, Button } from 'react-native-paper'; +import DateTimePicker from '@react-native-community/datetimepicker'; -import { useFormik } from "formik"; -import i18n from "i18n-js"; -import PropTypes from "prop-types"; -import * as Yup from "yup"; -import { useTheme, TextInput } from "react-native-paper"; -import DateTimePicker from "@react-native-community/datetimepicker"; +import { FormButtons, ValidationMessage, AbpSelect } from '../../../../components'; +import { useThemeColors } from '../../../../hooks'; -import { FormButtons } from "../../../../components/FormButtons"; -import ValidationMessage from "../../../../components/ValidationMessage/ValidationMessage"; -import AbpSelect from "../../../../components/Select/Select"; const validations = { name: Yup.string().required("AbpValidation::ThisFieldIsRequired."), @@ -412,19 +425,19 @@ const props = { }; function CreateUpdateBookForm({ submit }) { - const theme = useTheme(); + const { primaryContainer, background, onBackground } = useThemeColors(); const [bookTypeVisible, setBookTypeVisible] = useState(false); const [publishDateVisible, setPublishDateVisible] = useState(false); - const nameRef = useRef(); - const priceRef = useRef(); - const typeRef = useRef(); - const publishDateRef = useRef(); + const nameRef = useRef(null); + const priceRef = useRef(null); + const typeRef = useRef(null); + const publishDateRef = useRef(null); const inputStyle = { ...styles.input, - backgroundColor: theme.colors.primaryContainer, + backgroundColor: primaryContainer, }; const bookTypes = new Array(8).fill(0).map((_, i) => ({ id: i + 1, @@ -479,7 +492,7 @@ function CreateUpdateBookForm({ submit }) { }; return ( - + - {publishDateVisible && ( - - )} - - + - + priceRef.current.focus()} returnKeyType="next" - onChangeText={bookForm.handleChange("name")} - onBlur={bookForm.handleBlur("name")} + onChangeText={bookForm.handleChange('name')} + onBlur={bookForm.handleBlur('name')} value={bookForm.values.name} autoCapitalize="none" - label={i18n.t("BookStore::Name")} + label={i18n.t('BookStore::Name')} style={inputStyle} {...props} /> - {isInvalidControl("name") && ( - {bookForm.errors.name} + {isInvalidControl('name') && ( + {bookForm.errors.name as string} )} - + typeRef.current.focus()} returnKeyType="next" - onChangeText={bookForm.handleChange("price")} - onBlur={bookForm.handleBlur("price")} + onChangeText={bookForm.handleChange('price')} + onBlur={bookForm.handleBlur('price')} value={bookForm.values.price} autoCapitalize="none" - label={i18n.t("BookStore::Price")} + label={i18n.t('BookStore::Price')} style={inputStyle} {...props} /> - {isInvalidControl("price") && ( - {bookForm.errors.price} + {isInvalidControl('price') && ( + {bookForm.errors.price as string} )} - + setBookTypeVisible(true)} - icon="menu-down" - /> - } + error={isInvalidControl('type')} + label={i18n.t('BookStore::Type')} + right={ setBookTypeVisible(true)} icon="menu-down" />} style={inputStyle} editable={false} value={bookForm.values.typeDisplayName} {...props} /> - {isInvalidControl("type") && ( - {bookForm.errors.type} + {isInvalidControl('type') && ( + {bookForm.errors.type as string} )} - + setPublishDateVisible(true)} - icon="menu-down" + setPublishDateVisible(true)} + icon="calendar" + iconColor={bookForm.values.publishDate ? '#4CAF50' : '#666'} /> } style={inputStyle} editable={false} - value={bookForm.values.publishDate?.toLocaleDateString()} + value={formatDate(bookForm.values.publishDate)} + placeholder="Select publish date" {...props} /> - {isInvalidControl("publishDate") && ( - - {bookForm.errors.publishDate} - + {isInvalidControl('publishDate') && ( + {bookForm.errors.publishDate as string} )} + + + + {i18n.t('BookStore::PublishDate')} + + + + + + + + + + @@ -602,12 +629,12 @@ function CreateUpdateBookForm({ submit }) { } const styles = StyleSheet.create({ + inputContainer: { + margin: 8, + marginLeft: 16, + marginRight: 16, + }, input: { - container: { - margin: 8, - marginLeft: 16, - marginRight: 16, - }, borderRadius: 8, borderTopLeftRadius: 8, borderTopRightRadius: 8, @@ -616,9 +643,38 @@ const styles = StyleSheet.create({ marginLeft: 16, marginRight: 16, }, + dateModal: { + padding: 20, + margin: 20, + borderRadius: 12, + elevation: 5, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + }, + modalTitle: { + textAlign: 'center', + marginBottom: 16, + fontWeight: '600', + }, + divider: { + marginBottom: 16, + }, + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, + paddingHorizontal: 8, + }, }); CreateUpdateBookForm.propTypes = { + book: PropTypes.object, + authors: PropTypes.array.isRequired, submit: PropTypes.func.isRequired, }; @@ -636,9 +692,9 @@ export default CreateUpdateBookForm; ## Update a Book -We need the navigation parameter for the get bookId and then navigate it again after the Create & Update operation. That's why we'll pass the navigation parameter to the `BooksScreen` component. +We need the navigation parameter for getting the bookId and then navigate it again after the create & update operations. That is why we will pass the navigation parameter to the `BooksScreen` component. -```js +```tsx //Imports.. //Add navigation parameter @@ -657,66 +713,102 @@ function BookStoreScreen({ navigation }) { export default BookStoreScreen; ``` -Replace the code below in the `BookScreen.js` file under the `./src/screens/BookStore/Books` folder. +Replace the code below in the `BookScreen.tsx` file under the `./src/screens/BookStore/Books` folder. -```js -import React from "react"; -import { useSelector } from "react-redux"; -import { Alert, View, StyleSheet } from "react-native"; -import { useTheme, List, IconButton, AnimatedFAB } from "react-native-paper"; -import { useActionSheet } from "@expo/react-native-action-sheet"; -import i18n from "i18n-js"; +```tsx +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Alert, View, StyleSheet } from 'react-native'; +import { List, IconButton, AnimatedFAB } from 'react-native-paper'; +import { useActionSheet } from '@expo/react-native-action-sheet'; +import i18n from 'i18n-js'; -import { getList } from "../../../api/BookAPI"; -import DataList from "../../../components/DataList/DataList"; -import { createAppConfigSelector } from "../../../store/selectors/AppSelectors"; +import { getList, remove } from '../../../api/BookAPI'; +import { DataList } from '../../../components'; +import { createAppConfigSelector } from '../../../store/selectors/AppSelectors'; +import { useThemeColors } from '../../../hooks'; function BooksScreen({ navigation }) { - const theme = useTheme(); + const { background, primary } = useThemeColors(); const currentUser = useSelector(createAppConfigSelector())?.currentUser; + const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies; + + const [refresh, setRefresh] = useState(null); const { showActionSheetWithOptions } = useActionSheet(); - const openContextMenu = (item) => { + const openContextMenu = (item: { id: string }) => { const options = []; - options.push(i18n.t("AbpUi::Edit")); - options.push(i18n.t("AbpUi::Cancel")); + if (policies['BookStore.Books.Delete']) { + options.push(i18n.t('AbpUi::Delete')); + } + + if (policies['BookStore.Books.Edit']) { + options.push(i18n.t('AbpUi::Edit')); + } + + options.push(i18n.t('AbpUi::Cancel')); showActionSheetWithOptions( { options, cancelButtonIndex: options.length - 1, + destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')), }, - (index) => { + index => { switch (options[index]) { - case i18n.t("AbpUi::Edit"): + case i18n.t('AbpUi::Edit'): edit(item); break; + case i18n.t('AbpUi::Delete'): + removeOnClick(item); + break; } - } + }, ); }; - const edit = (item) => { - navigation.navigate("CreateUpdateBook", { bookId: item.id }); + const removeOnClick = (item: { id: string }) => { + Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [ + { + text: i18n.t('AbpUi::Cancel'), + style: 'cancel', + }, + { + style: 'default', + text: i18n.t('AbpUi::Ok'), + onPress: () => { + remove(item.id).then(() => { + setRefresh((refresh ?? 0) + 1); + }); + }, + }, + ]); + }; + + const edit = (item: { id: string }) => { + navigation.navigate('CreateUpdateBook', { bookId: item.id }); }; return ( - + {currentUser?.isAuthenticated && ( ( ( + description={`${item.authorName} | ${i18n.t( + 'BookStore::Enum:BookType.' + item.type, + )}`} + right={props => ( openContextMenu(item)} /> @@ -726,17 +818,17 @@ function BooksScreen({ navigation }) { /> )} - {currentUser?.isAuthenticated && ( + {currentUser?.isAuthenticated && !!policies['BookStore.Books.Create'] && ( navigation.navigate("CreateUpdateBook")} + onPress={() => navigation.navigate('CreateUpdateBook')} visible={true} - animateFrom={"right"} - iconMode={"static"} - style={[styles.fabStyle, { backgroundColor: theme.colors.primary }]} + animateFrom={'right'} + iconMode={'static'} + style={[styles.fabStyle, { backgroundColor: primary }]} /> )} @@ -750,36 +842,31 @@ const styles = StyleSheet.create({ fabStyle: { bottom: 16, right: 16, - position: "absolute", + position: 'absolute', }, }); export default BooksScreen; ``` -Replace code below for `CreateUpdateBookScreen.js` file under the `./src/screens/BookStore/Books/CreateUpdateBook/` +Replace code below for `CreateUpdateBookScreen.tsx` file under the `./src/screens/BookStore/Books/CreateUpdateBook/` -```js -import PropTypes from "prop-types"; -import React, { useEffect, useState } from "react"; +```tsx +import PropTypes from 'prop-types'; +import { useEffect, useState } from 'react'; -import { get, create, update } from "../../../../api/BookAPI"; -import LoadingActions from "../../../../store/actions/LoadingActions"; -import { createLoadingSelector } from "../../../../store/selectors/LoadingSelectors"; -import { connectToRedux } from "../../../../utils/ReduxConnect"; -import CreateUpdateBookForm from "./CreateUpdateBookForm"; +import { getAuthorLookup, get, create, update } from '../../../../api/BookAPI'; +import LoadingActions from '../../../../store/actions/LoadingActions'; +import { createLoadingSelector } from '../../../../store/selectors/LoadingSelectors'; +import { connectToRedux } from '../../../../utils/ReduxConnect'; +import CreateUpdateBookForm from './CreateUpdateBookForm'; -function CreateUpdateBookScreen({ - navigation, - route, - startLoading, - clearLoading, -}) { +function CreateUpdateBookScreen({ navigation, route, startLoading, clearLoading }) { const { bookId } = route.params || {}; const [book, setBook] = useState(null); - const submit = (data) => { - startLoading({ key: "save" }); + const submit = (data: any) => { + startLoading({ key: 'save' }); (data.id ? update(data, data.id) : create(data)) .then(() => navigation.goBack()) @@ -788,10 +875,10 @@ function CreateUpdateBookScreen({ useEffect(() => { if (bookId) { - startLoading({ key: "fetchBookDetail" }); + startLoading({ key: 'fetchBookDetail' }); get(bookId) - .then((response) => setBook(response)) + .then((response: any) => setBook(response)) .finally(() => clearLoading()); } }, [bookId]); @@ -806,7 +893,7 @@ CreateUpdateBookScreen.propTypes = { export default connectToRedux({ component: CreateUpdateBookScreen, - stateProps: (state) => ({ loading: createLoadingSelector()(state) }), + stateProps: state => ({ loading: createLoadingSelector()(state) }), dispatchProps: { startLoading: LoadingActions.start, clearLoading: LoadingActions.clear, @@ -818,9 +905,9 @@ export default connectToRedux({ - `update` method is used to update the book on the server. - `route` parameter will be used to get the bookId from the navigation. -Replace the `CreateUpdateBookForm.js` file with the code below. We'll use this file for the create and update operations. +Replace the `CreateUpdateBookForm.tsx` file with the code below. We will use this file for the create and update operations. -```js +```tsx //Imports.. //validateSchema @@ -859,7 +946,7 @@ function CreateUpdateBookForm({ //Other codes.. ``` -- `book` is a nullable property. It'll store the selected book, if the book parameter is null then we'll create a new book. +- `book` is a nullable property. It will store the selected book, if the book parameter is null then we will create a new book. ![Book List With Options](../../../images/book-list-with-options.png) @@ -867,62 +954,70 @@ function CreateUpdateBookForm({ ## Delete a Book -Replace the code below in the `BooksScreen.js` file under the `./src/screens/BookStore/Books` folder. +Replace the code below in the `BooksScreen.tsx` file under the `./src/screens/BookStore/Books` folder. -```js -import React, { useState } from "react"; -import { useSelector } from "react-redux"; -import { Alert, View, StyleSheet } from "react-native"; -import { useTheme, List, IconButton, AnimatedFAB } from "react-native-paper"; -import { useActionSheet } from "@expo/react-native-action-sheet"; -import i18n from "i18n-js"; +```tsx +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Alert, View, StyleSheet } from 'react-native'; +import { List, IconButton, AnimatedFAB } from 'react-native-paper'; +import { useActionSheet } from '@expo/react-native-action-sheet'; +import i18n from 'i18n-js'; -import { getList, remove } from "../../../api/BookAPI"; -import DataList from "../../../components/DataList/DataList"; -import { createAppConfigSelector } from "../../../store/selectors/AppSelectors"; +import { getList, remove } from '../../../api/BookAPI'; +import { DataList } from '../../../components'; +import { createAppConfigSelector } from '../../../store/selectors/AppSelectors'; +import { useThemeColors } from '../../../hooks'; function BooksScreen({ navigation }) { - const theme = useTheme(); + const { background, primary } = useThemeColors(); const currentUser = useSelector(createAppConfigSelector())?.currentUser; + const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies; const [refresh, setRefresh] = useState(null); const { showActionSheetWithOptions } = useActionSheet(); - const openContextMenu = (item) => { + const openContextMenu = (item: { id: string }) => { const options = []; - options.push(i18n.t("AbpUi::Delete")); - options.push(i18n.t("AbpUi::Edit")); - options.push(i18n.t("AbpUi::Cancel")); + if (policies['BookStore.Books.Delete']) { + options.push(i18n.t('AbpUi::Delete')); + } + + if (policies['BookStore.Books.Edit']) { + options.push(i18n.t('AbpUi::Edit')); + } + + options.push(i18n.t('AbpUi::Cancel')); showActionSheetWithOptions( { options, cancelButtonIndex: options.length - 1, - destructiveButtonIndex: options.indexOf(i18n.t("AbpUi::Delete")), + destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')), }, - (index) => { + index => { switch (options[index]) { - case i18n.t("AbpUi::Edit"): + case i18n.t('AbpUi::Edit'): edit(item); break; - case i18n.t("AbpUi::Delete"): + case i18n.t('AbpUi::Delete'): removeOnClick(item); break; } - } + }, ); }; - const removeOnClick = (item) => { - Alert.alert("Warning", i18n.t("BookStore::AreYouSureToDelete"), [ + const removeOnClick = (item: { id: string }) => { + Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [ { - text: i18n.t("AbpUi::Cancel"), - style: "cancel", + text: i18n.t('AbpUi::Cancel'), + style: 'cancel', }, { - style: "default", - text: i18n.t("AbpUi::Ok"), + style: 'default', + text: i18n.t('AbpUi::Ok'), onPress: () => { remove(item.id).then(() => { setRefresh((refresh ?? 0) + 1); @@ -932,12 +1027,12 @@ function BooksScreen({ navigation }) { ]); }; - const edit = (item) => { - navigation.navigate("CreateUpdateBook", { bookId: item.id }); + const edit = (item: { id: string }) => { + navigation.navigate('CreateUpdateBook', { bookId: item.id }); }; return ( - + {currentUser?.isAuthenticated && ( ( + description={`${item.authorName} | ${i18n.t( + 'BookStore::Enum:BookType.' + item.type, + )}`} + right={props => ( openContextMenu(item)} /> @@ -962,17 +1059,17 @@ function BooksScreen({ navigation }) { /> )} - {currentUser?.isAuthenticated && ( + {currentUser?.isAuthenticated && !!policies['BookStore.Books.Create'] && ( navigation.navigate("CreateUpdateBook")} + onPress={() => navigation.navigate('CreateUpdateBook')} visible={true} - animateFrom={"right"} - iconMode={"static"} - style={[styles.fabStyle, { backgroundColor: theme.colors.primary }]} + animateFrom={'right'} + iconMode={'static'} + style={[styles.fabStyle, { backgroundColor: primary }]} /> )} @@ -986,7 +1083,7 @@ const styles = StyleSheet.create({ fabStyle: { bottom: 16, right: 16, - position: "absolute", + position: 'absolute', }, }); @@ -1006,7 +1103,7 @@ export default BooksScreen; Add `grantedPolicies` to the policies variable from the `appConfig` store -```js +```tsx //Other imports.. import { useSelector } from "react-redux"; @@ -1065,9 +1162,9 @@ export default BookStoreScreen; ### Hide the New Book Button -`New Book` button is placed in the BooksScreen as a `+` icon button. For the toggle visibility of the button, we need to add the `policies` variable to the `BooksScreen` component like the `BookStoreScreen` component. Open the `BooksScreen.js` file in the `./src/screens/BookStore/Books` folder and include the code below. +`New Book` button is placed in the BooksScreen as a `+` icon button. For the toggle visibility of the button, we need to add the `policies` variable to the `BooksScreen` component like the `BookStoreScreen` component. Open the `BooksScreen.tsx` file in the `./src/screens/BookStore/Books` folder and include the code below. -```js +```tsx //Imports.. function BooksScreen({ navigation }) { @@ -1090,7 +1187,7 @@ function BooksScreen({ navigation }) { visible={true} animateFrom={'right'} iconMode={'static'} - style={[styles.fabStyle, { backgroundColor: theme.colors.primary }]} + style={[styles.fabStyle, { backgroundColor: primary }]} /> ) } @@ -1104,9 +1201,9 @@ function BooksScreen({ navigation }) { ### Hide the Edit and Delete Actions -Update your code as below in the `./src/screens/BookStore/Books/BooksScreen.js` file. We'll check the `policies` variables for the `Edit` and `Delete` actions. +Update your code as below in the `./src/screens/BookStore/Books/BooksScreen.tsx` file. We'll check the `policies` variables for the `Edit` and `Delete` actions. -```js +```tsx function BooksScreen() { //... @@ -1134,8 +1231,8 @@ function BooksScreen() { ### Create API Proxy -```js -./src/api/AuthorAPI.js +```ts +//./src/api/AuthorAPI.ts import api from './API'; @@ -1154,9 +1251,9 @@ export const remove = id => api.delete(`/api/app/author/${id}`).then(({ data }) ### Add Authors Tab to BookStoreScreen -Open the `./src/screens/BookStore/BookStoreScreen.js` file and update it with the code below. +Open the `./src/screens/BookStore/BookStoreScreen.tsx` file and update it with the code below. -```js +```tsx //Other imports import AuthorsScreen from "./Authors/AuthorsScreen"; @@ -1186,70 +1283,70 @@ function BookStoreScreen({ navigation }) { export default BookStoreScreen; ``` -Create a `AuthorsScreen.js` file under the `./src/screens/BookStore/Authors` folder and add the code below to it. +Create a `AuthorsScreen.tsx` file under the `./src/screens/BookStore/Authors` folder and add the code below to it. -```js -import React, { useState } from "react"; -import { useSelector } from "react-redux"; -import { Alert, View, StyleSheet } from "react-native"; -import { useTheme, List, IconButton, AnimatedFAB } from "react-native-paper"; -import { useActionSheet } from "@expo/react-native-action-sheet"; -import i18n from "i18n-js"; +```tsx +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Alert, View, StyleSheet } from 'react-native'; +import { List, IconButton, AnimatedFAB } from 'react-native-paper'; +import { useActionSheet } from '@expo/react-native-action-sheet'; +import i18n from 'i18n-js'; -import { getList, remove } from "../../../api/AuthorAPI"; -import DataList from "../../../components/DataList/DataList"; -import { createAppConfigSelector } from "../../../store/selectors/AppSelectors"; +import { getList, remove } from '../../../api/AuthorAPI'; +import { DataList } from '../../../components'; +import { createAppConfigSelector } from '../../../store/selectors/AppSelectors'; +import { useThemeColors } from '../../../hooks'; function AuthorsScreen({ navigation }) { - const theme = useTheme(); + const { background, primary } = useThemeColors(); const currentUser = useSelector(createAppConfigSelector())?.currentUser; - const policies = useSelector(createAppConfigSelector())?.auth - ?.grantedPolicies; + const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies; const [refresh, setRefresh] = useState(null); const { showActionSheetWithOptions } = useActionSheet(); - const openContextMenu = (item) => { + const openContextMenu = (item: { id: string }) => { const options = []; - if (policies["BookStore.Authors.Delete"]) { - options.push(i18n.t("AbpUi::Delete")); + if (policies['BookStore.Authors.Delete']) { + options.push(i18n.t('AbpUi::Delete')); } - if (policies["BookStore.Authors.Edit"]) { - options.push(i18n.t("AbpUi::Edit")); + if (policies['BookStore.Authors.Edit']) { + options.push(i18n.t('AbpUi::Edit')); } - options.push(i18n.t("AbpUi::Cancel")); + options.push(i18n.t('AbpUi::Cancel')); showActionSheetWithOptions( { options, cancelButtonIndex: options.length - 1, - destructiveButtonIndex: options.indexOf(i18n.t("AbpUi::Delete")), + destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')), }, - (index) => { + (index: number) => { switch (options[index]) { - case i18n.t("AbpUi::Edit"): + case i18n.t('AbpUi::Edit'): edit(item); break; - case i18n.t("AbpUi::Delete"): + case i18n.t('AbpUi::Delete'): removeOnClick(item); break; } - } + }, ); }; - const removeOnClick = ({ id } = {}) => { - Alert.alert("Warning", i18n.t("BookStore::AreYouSureToDelete"), [ + const removeOnClick = ({ id }: { id: string }) => { + Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [ { - text: i18n.t("AbpUi::Cancel"), - style: "cancel", + text: i18n.t('AbpUi::Cancel'), + style: 'cancel', }, { - style: "default", - text: i18n.t("AbpUi::Ok"), + style: 'default', + text: i18n.t('AbpUi::Ok'), onPress: () => { remove(id).then(() => { setRefresh((refresh ?? 0) + 1); @@ -1259,12 +1356,12 @@ function AuthorsScreen({ navigation }) { ]); }; - const edit = ({ id } = {}) => { - navigation.navigate("CreateUpdateAuthor", { authorId: id }); + const edit = ({ id }: { id: string }) => { + navigation.navigate('CreateUpdateAuthor', { authorId: id }); }; return ( - + {currentUser?.isAuthenticated && ( ( + description={item.shortBio || new Date(item.birthDate)?.toLocaleDateString()} + right={(props: any) => ( openContextMenu(item)} /> @@ -1291,17 +1386,17 @@ function AuthorsScreen({ navigation }) { /> )} - {currentUser?.isAuthenticated && policies["BookStore.Authors.Create"] && ( + {currentUser?.isAuthenticated && policies['BookStore.Authors.Create'] && ( navigation.navigate("CreateUpdateAuthor")} + onPress={() => navigation.navigate('CreateUpdateAuthor')} visible={true} - animateFrom={"right"} - iconMode={"static"} - style={[styles.fabStyle, { backgroundColor: theme.colors.primary }]} + animateFrom={'right'} + iconMode={'static'} + style={[styles.fabStyle, { backgroundColor: primary }]} /> )} @@ -1315,36 +1410,31 @@ const styles = StyleSheet.create({ fabStyle: { bottom: 16, right: 16, - position: "absolute", + position: 'absolute', }, }); export default AuthorsScreen; ``` -Create a `CreateUpdateAuthorScreen.js` file under the `./src/screens/BookStore/Authors/CreateUpdateAuthor` folder and add the code below to it. +Create a `CreateUpdateAuthorScreen.tsx` file under the `./src/screens/BookStore/Authors/CreateUpdateAuthor` folder and add the code below to it. -```js -import PropTypes from "prop-types"; -import React, { useEffect, useState } from "react"; +```tsx +import PropTypes from 'prop-types'; +import { useEffect, useState } from 'react'; -import { get, create, update } from "../../../../api/AuthorAPI"; -import LoadingActions from "../../../../store/actions/LoadingActions"; -import { createLoadingSelector } from "../../../../store/selectors/LoadingSelectors"; -import { connectToRedux } from "../../../../utils/ReduxConnect"; -import CreateUpdateAuthorForm from "./CreateUpdateAuthorForm"; +import { get, create, update } from '../../../../api/AuthorAPI'; +import LoadingActions from '../../../../store/actions/LoadingActions'; +import { createLoadingSelector } from '../../../../store/selectors/LoadingSelectors'; +import { connectToRedux } from '../../../../utils/ReduxConnect'; +import CreateUpdateAuthorForm from './CreateUpdateAuthorForm'; -function CreateUpdateAuthorScreen({ - navigation, - route, - startLoading, - clearLoading, -}) { +function CreateUpdateAuthorScreen({ navigation, route, startLoading, clearLoading }) { const { authorId } = route.params || {}; - const [author, setAuthor] = useState(null); + const [ author, setAuthor ] = useState(null); - const submit = (data) => { - startLoading({ key: "save" }); + const submit = (data: any) => { + startLoading({ key: 'save' }); (data.id ? update(data, data.id) : create(data)) .then(() => navigation.goBack()) @@ -1353,10 +1443,10 @@ function CreateUpdateAuthorScreen({ useEffect(() => { if (authorId) { - startLoading({ key: "fetchAuthorDetail" }); + startLoading({ key: 'fetchAuthorDetail' }); get(authorId) - .then((response) => setAuthor(response)) + .then((response: any) => setAuthor(response)) .finally(() => clearLoading()); } }, [authorId]); @@ -1371,7 +1461,7 @@ CreateUpdateAuthorScreen.propTypes = { export default connectToRedux({ component: CreateUpdateAuthorScreen, - stateProps: (state) => ({ loading: createLoadingSelector()(state) }), + stateProps: (state: any) => ({ loading: createLoadingSelector()(state) }), dispatchProps: { startLoading: LoadingActions.start, clearLoading: LoadingActions.clear, @@ -1379,55 +1469,44 @@ export default connectToRedux({ }); ``` -Create a `CreateUpdateAuthorForm.js` file under the `./src/screens/BookStore/Authors/CreateUpdateAuthor` folder and add the code below to it. +Create a `CreateUpdateAuthorForm.tsx` file under the `./src/screens/BookStore/Authors/CreateUpdateAuthor` folder and add the code below to it. -```js -import React, { useRef, useState } from "react"; -import { - Platform, - KeyboardAvoidingView, - StyleSheet, - View, - ScrollView, -} from "react-native"; +```tsx +import { useRef, useState } from 'react'; +import { Platform, KeyboardAvoidingView, StyleSheet, View, ScrollView } from 'react-native'; -import { useFormik } from "formik"; -import i18n from "i18n-js"; -import PropTypes from "prop-types"; -import * as Yup from "yup"; -import { useTheme, TextInput } from "react-native-paper"; -import DateTimePicker from "@react-native-community/datetimepicker"; +import { useFormik } from 'formik'; +import i18n from 'i18n-js'; +import PropTypes from 'prop-types'; +import * as Yup from 'yup'; +import { Divider, Portal, TextInput, Text, Button, Modal } from 'react-native-paper'; +import DateTimePicker from '@react-native-community/datetimepicker'; -import { FormButtons } from "../../../../components/FormButtons"; -import ValidationMessage from "../../../../components/ValidationMessage/ValidationMessage"; +import { useThemeColors } from '../../../../hooks'; +import { FormButtons, ValidationMessage } from '../../../../components'; const validations = { - name: Yup.string().required("AbpValidation::ThisFieldIsRequired."), - birthDate: Yup.string() - .nullable() - .required("AbpValidation::ThisFieldIsRequired."), + name: Yup.string().required('AbpValidation::ThisFieldIsRequired.'), + birthDate: Yup.string().nullable().required('AbpValidation::ThisFieldIsRequired.'), }; const props = { - underlineStyle: { backgroundColor: "transparent" }, - underlineColor: "#333333bf", + underlineStyle: { backgroundColor: 'transparent' }, + underlineColor: '#333333bf', }; function CreateUpdateAuthorForm({ submit, author = null }) { - const theme = useTheme(); + const { primaryContainer, background, onBackground } = useThemeColors(); const [birthDateVisible, setPublishDateVisible] = useState(false); - const nameRef = useRef(); - const birthDateRef = useRef(); - const shortBioRef = useRef(); + const nameRef = useRef(null); + const birthDateRef = useRef(null); + const shortBioRef = useRef(null); - const inputStyle = { - ...styles.input, - backgroundColor: theme.colors.primaryContainer, - }; + const inputStyle = { ...styles.input, backgroundColor: primaryContainer }; - const onSubmit = (values) => { + const onSubmit = (values: any) => { if (!authorForm.isValid) { return; } @@ -1443,9 +1522,9 @@ function CreateUpdateAuthorForm({ submit, author = null }) { }), initialValues: { ...author, - name: author?.name || "", + name: author?.name || '', birthDate: (author?.birthDate && new Date(author?.birthDate)) || null, - shortBio: author?.shortBio || "", + shortBio: author?.shortBio || '', }, onSubmit, }); @@ -1462,89 +1541,110 @@ function CreateUpdateAuthorForm({ submit, author = null }) { ); }; - const onChange = (event, selectedDate) => { + const onChange = (event: any, selectedDate: any) => { if (!selectedDate) { return; } setPublishDateVisible(false); - if (event && event.type !== "dismissed") { - authorForm.setFieldValue("birthDate", selectedDate, true); + if (event && event.type !== 'dismissed') { + authorForm.setFieldValue('birthDate', selectedDate, true); } }; return ( - + {birthDateVisible && ( )} - + - + birthDateRef.current.focus()} returnKeyType="next" - onChangeText={authorForm.handleChange("name")} - onBlur={authorForm.handleBlur("name")} + onChangeText={authorForm.handleChange('name')} + onBlur={authorForm.handleBlur('name')} value={authorForm.values.name} autoCapitalize="none" - label={i18n.t("BookStore::Name")} + label={i18n.t('BookStore::Name')} style={inputStyle} {...props} /> - {isInvalidControl("name") && ( - {authorForm.errors.name} + {isInvalidControl('name') && ( + {authorForm.errors.name as string} )} - + shortBioRef.current.focus()} right={ - setPublishDateVisible(true)} - icon="menu-down" - /> + setPublishDateVisible(true)} icon="calendar" /> } style={inputStyle} editable={false} value={authorForm.values.birthDate?.toLocaleDateString()} {...props} /> - {isInvalidControl("birthDate") && ( - - {authorForm.errors.birthDate} - + {isInvalidControl('birthDate') && ( + {authorForm.errors.birthDate as string} )} - + + + + {i18n.t('BookStore::BirthDate')} + + + + + + + + + + + authorForm.handleSubmit()} returnKeyType="next" - onChangeText={authorForm.handleChange("shortBio")} - onBlur={authorForm.handleBlur("shortBio")} + onChangeText={authorForm.handleChange('shortBio')} + onBlur={authorForm.handleBlur('shortBio')} value={authorForm.values.shortBio} autoCapitalize="none" - label={i18n.t("BookStore::ShortBio")} + label={i18n.t('BookStore::ShortBio')} style={inputStyle} {...props} /> @@ -1558,12 +1658,12 @@ function CreateUpdateAuthorForm({ submit, author = null }) { } const styles = StyleSheet.create({ - input: { - container: { - margin: 8, + inputContainer: { + margin: 8, marginLeft: 16, marginRight: 16, - }, + }, + input: { borderRadius: 8, borderTopLeftRadius: 8, borderTopRightRadius: 8, @@ -1572,6 +1672,33 @@ const styles = StyleSheet.create({ marginLeft: 16, marginRight: 16, }, + divider: { + marginBottom: 16, + }, + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, + paddingHorizontal: 8, + }, + dateModal: { + padding: 20, + margin: 20, + borderRadius: 12, + elevation: 5, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + }, + modalTitle: { + textAlign: 'center', + marginBottom: 16, + fontWeight: '600', + }, }); CreateUpdateAuthorForm.propTypes = { @@ -1596,7 +1723,7 @@ export default CreateUpdateAuthorForm; Update BookAPI proxy file and include `getAuthorLookup` method -```js +```ts import api from "./API"; export const getList = () => api.get("/api/app/book").then(({ data }) => data); @@ -1621,9 +1748,9 @@ export const remove = (id) => ### Add `AuthorName` to the Book List -Open `BooksScreen.js` file under the `./src/screens/BookStore/Books` and update code below. +Open `BooksScreen.tsx` file under the `./src/screens/BookStore/Books` and update code below. -```js +```tsx //Improts function BooksScreen({ navigation }) { @@ -1665,7 +1792,7 @@ function BooksScreen({ navigation }) { ### Pass authors to the `CreateUpdateBookForm` -```js +```tsx import { getAuthorLookup, //Add this line get, @@ -1699,7 +1826,7 @@ function CreateUpdateBookScreen({ ### Add `authorId` field to Book Form -```js +```tsx const validations = { authorId: Yup.string() .nullable() @@ -1734,7 +1861,7 @@ function CreateUpdateBookForm({ submit, book = null, authors = [] }) { //Add `AbpSelect` component and TextInput for authors return ( - + @@ -1798,4 +1925,4 @@ export default CreateUpdateBookForm; ![Authors in Book Form](../../../images/authors-in-book-form.png) -That's all. Just run the application and try to create or edit an author. +That is all. Just run the application and try to create or edit an author. diff --git a/framework/Volo.Abp.sln.DotSettings b/framework/Volo.Abp.sln.DotSettings index 925b5c212a..db86e2eb6a 100644 --- a/framework/Volo.Abp.sln.DotSettings +++ b/framework/Volo.Abp.sln.DotSettings @@ -1,4 +1,5 @@  + AI SQL True D:\Github\abp\common.DotSettings diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index 990f998b5b..ab63be61ea 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -62,6 +62,7 @@ + @@ -163,7 +164,8 @@ - + + @@ -189,6 +191,7 @@ + @@ -248,6 +251,5 @@ - - \ No newline at end of file + diff --git a/framework/src/Volo.Abp.AI.Abstractions/FodyWeavers.xml b/framework/src/Volo.Abp.AI.Abstractions/FodyWeavers.xml new file mode 100644 index 0000000000..bc5a74a236 --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AI.Abstractions/FodyWeavers.xsd b/framework/src/Volo.Abp.AI.Abstractions/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo.Abp.AI.Abstractions.abppkg b/framework/src/Volo.Abp.AI.Abstractions/Volo.Abp.AI.Abstractions.abppkg new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo.Abp.AI.Abstractions.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo.Abp.AI.Abstractions.csproj b/framework/src/Volo.Abp.AI.Abstractions/Volo.Abp.AI.Abstractions.csproj new file mode 100644 index 0000000000..f5240e5187 --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo.Abp.AI.Abstractions.csproj @@ -0,0 +1,26 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + enable + Nullable + Volo.Abp.AI.Abstractions + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/AbpAIAbstractionsModule.cs b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/AbpAIAbstractionsModule.cs new file mode 100644 index 0000000000..cc478d8503 --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/AbpAIAbstractionsModule.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AI; + +public class AbpAIAbstractionsModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IChatClient.cs b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IChatClient.cs new file mode 100644 index 0000000000..e7458af657 --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IChatClient.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.AI; + +namespace Volo.Abp.AI; + +public interface IChatClient : IChatClient + where TWorkSpace : class +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IKernelAccessor.cs b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IKernelAccessor.cs new file mode 100644 index 0000000000..7f55ad9e6f --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/IKernelAccessor.cs @@ -0,0 +1,14 @@ + +using Microsoft.SemanticKernel; + +namespace Volo.Abp.AI; + +public interface IKernelAccessor +{ + Kernel? Kernel { get; } +} + +public interface IKernelAccessor : IKernelAccessor + where TWorkSpace : class +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/WorkspaceNameAttribute.cs b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/WorkspaceNameAttribute.cs new file mode 100644 index 0000000000..1cf34d0781 --- /dev/null +++ b/framework/src/Volo.Abp.AI.Abstractions/Volo/Abp/AI/WorkspaceNameAttribute.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Collections.Concurrent; + +namespace Volo.Abp.AI; + +[AttributeUsage(AttributeTargets.Class)] +public class WorkspaceNameAttribute : Attribute +{ + public string Name { get; } + + public WorkspaceNameAttribute(string name) + { + Check.NotNull(name, nameof(name)); + + Name = name; + } + + private static readonly ConcurrentDictionary _nameCache = new(); + + public static string GetWorkspaceName() + { + return GetWorkspaceName(typeof(TWorkspace)); + } + + public static string GetWorkspaceName(Type workspaceType) + { + return _nameCache.GetOrAdd(workspaceType, type => + { + var workspaceNameAttribute = type + .GetCustomAttributes(true) + .OfType() + .FirstOrDefault(); + + return workspaceNameAttribute?.Name ?? type.FullName!; + }); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/FodyWeavers.xml b/framework/src/Volo.Abp.AI/FodyWeavers.xml new file mode 100644 index 0000000000..bc5a74a236 --- /dev/null +++ b/framework/src/Volo.Abp.AI/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AI/FodyWeavers.xsd b/framework/src/Volo.Abp.AI/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.AI/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo.Abp.AI.abppkg b/framework/src/Volo.Abp.AI/Volo.Abp.AI.abppkg new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo.Abp.AI.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo.Abp.AI.csproj b/framework/src/Volo.Abp.AI/Volo.Abp.AI.csproj new file mode 100644 index 0000000000..54a9c047b2 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo.Abp.AI.csproj @@ -0,0 +1,27 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + enable + Nullable + Volo.Abp.AI + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs new file mode 100644 index 0000000000..f13a6e6f02 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AI; + +[DependsOn( + typeof(AbpAIAbstractionsModule) +)] +public class AbpAIModule : AbpModule +{ + public const string DefaultWorkspaceName = "Default"; + + public override void PostConfigureServices(ServiceConfigurationContext context) + { + var options = context.Services.ExecutePreConfiguredActions(); + + context.Services.Configure(workspaceOptions => + { + workspaceOptions.ConfiguredWorkspaceNames.UnionWith(options.Workspaces.Select(x => x.Key).ToArray()); + }); + + foreach (var workspaceConfig in options.Workspaces.Values) + { + if (workspaceConfig.ChatClient?.Builder is null) + { + continue; + } + + foreach (var builderConfigurer in workspaceConfig.ChatClient.BuilderConfigurers) + { + builderConfigurer.Action(workspaceConfig.ChatClient.Builder!); + } + + context.Services.AddKeyedChatClient( + AbpAIOptions.GetChatClientServiceKeyName(workspaceConfig.Name), + provider => workspaceConfig.ChatClient.Builder!.Build(provider), + ServiceLifetime.Transient + ); + + if (workspaceConfig.Name == DefaultWorkspaceName) + { + context.Services.AddTransient(sp => sp.GetRequiredKeyedService( + AbpAIOptions.GetChatClientServiceKeyName(workspaceConfig.Name) + ) + ); + } + } + + context.Services.TryAddTransient(typeof(IChatClient<>), typeof(TypedChatClient<>)); + + foreach (var workspaceConfig in options.Workspaces.Values) + { + if (workspaceConfig.Kernel?.Builder is null) + { + continue; + } + + foreach (var builderConfigurer in workspaceConfig.Kernel.BuilderConfigurers) + { + builderConfigurer.Action(workspaceConfig.Kernel.Builder!); + } + + // TODO: Check if we can use transient instead of singleton for Kernel + context.Services.AddKeyedTransient( + AbpAIOptions.GetKernelServiceKeyName(workspaceConfig.Name), + (provider, _) => workspaceConfig.Kernel.Builder!.Build()); + + if (workspaceConfig.Name == DefaultWorkspaceName) + { + context.Services.AddTransient(sp => sp.GetRequiredKeyedService( + AbpAIOptions.GetKernelServiceKeyName(workspaceConfig.Name) + ) + ); + } + + if (workspaceConfig.ChatClient?.Builder is null) + { + context.Services.AddKeyedTransient( + AbpAIOptions.GetChatClientServiceKeyName(workspaceConfig.Name), + (sp, _) => sp.GetKeyedService(AbpAIOptions.GetKernelServiceKeyName(workspaceConfig.Name))? + .GetRequiredService() + ?? throw new InvalidOperationException("Kernel or IChatClient not found with workspace name: " + workspaceConfig.Name) + ); + } + } + + context.Services.TryAddTransient(typeof(IKernelAccessor<>), typeof(TypedKernelAccessor<>)); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIOptions.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIOptions.cs new file mode 100644 index 0000000000..b5639580d7 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIOptions.cs @@ -0,0 +1,19 @@ +namespace Volo.Abp.AI; + +public class AbpAIOptions +{ + public const string ChatClientServiceKeyNamePrefix = "Abp.AI.ChatClient_"; + public const string KernelServiceKeyNamePrefix = "Abp.AI.Kernel_"; + + public WorkspaceConfigurationDictionary Workspaces { get; } = new(); + + public static string GetChatClientServiceKeyName(string name) + { + return $"{ChatClientServiceKeyNamePrefix}{name}"; + } + + public static string GetKernelServiceKeyName(string name) + { + return $"{KernelServiceKeyNamePrefix}{name}"; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIWorkspaceOptions.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIWorkspaceOptions.cs new file mode 100644 index 0000000000..1fad742c04 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIWorkspaceOptions.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AI; + +public class AbpAIWorkspaceOptions +{ + public HashSet ConfiguredWorkspaceNames { get; } = new(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/BuilderConfigurerList.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/BuilderConfigurerList.cs new file mode 100644 index 0000000000..0ee5e5164d --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/BuilderConfigurerList.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.AI; + +namespace Volo.Abp.AI; + +public class BuilderConfigurerList : NamedActionList +{ + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientConfiguration.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientConfiguration.cs new file mode 100644 index 0000000000..830d6fd87c --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientConfiguration.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.Extensions.AI; + +namespace Volo.Abp.AI; + +public class ChatClientConfiguration +{ + public ChatClientBuilder? Builder { get; set; } + + public BuilderConfigurerList BuilderConfigurers { get; } = new(); + + // TODO: Base chat client (for inheriting a chat client configuration from some other one) + + public void ConfigureBuilder(Action configureAction) + { + BuilderConfigurers.Add(configureAction); + } + + public void ConfigureBuilder(string name, Action configureAction) + { + BuilderConfigurers.Add(name, configureAction); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs new file mode 100644 index 0000000000..5370a41dd1 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AI; + +[ExposeServices(typeof(IKernelAccessor))] +public class DefaultKernelAccessor : IKernelAccessor, ITransientDependency +{ + public Kernel? Kernel { get; } + + public DefaultKernelAccessor(IServiceProvider serviceProvider) + { + Kernel = serviceProvider.GetKeyedService( + AbpAIModule.DefaultWorkspaceName); + } +} diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/KernelBuilderConfigurerList.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/KernelBuilderConfigurerList.cs new file mode 100644 index 0000000000..fbccc77025 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/KernelBuilderConfigurerList.cs @@ -0,0 +1,9 @@ +using Microsoft.SemanticKernel; + +namespace Volo.Abp.AI; + +public class KernelBuilderConfigurerList : NamedActionList +{ +} + + diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/KernelConfiguration.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/KernelConfiguration.cs new file mode 100644 index 0000000000..4b78e1b54d --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/KernelConfiguration.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.SemanticKernel; + +namespace Volo.Abp.AI; + +public class KernelConfiguration +{ + public IKernelBuilder? Builder { get; set; } + + public KernelBuilderConfigurerList BuilderConfigurers { get; } = new(); + + public void ConfigureBuilder(Action configureAction) + { + BuilderConfigurers.Add(configureAction); + } + + public void ConfigureBuilder(string name, Action configureAction) + { + BuilderConfigurers.Add(name, configureAction); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs new file mode 100644 index 0000000000..c75d9b0c97 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.AI; + +public class TypedChatClient : DelegatingChatClient, IChatClient + where TWorkSpace : class +{ + public TypedChatClient(IServiceProvider serviceProvider) + : base( + serviceProvider.GetRequiredKeyedService( + AbpAIOptions.GetChatClientServiceKeyName( + WorkspaceNameAttribute.GetWorkspaceName())) + ) + { + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedKernelAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedKernelAccessor.cs new file mode 100644 index 0000000000..09d0132676 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedKernelAccessor.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; + +namespace Volo.Abp.AI; + +public class TypedKernelAccessor : IKernelAccessor + where TWorkSpace : class +{ + public Kernel? Kernel { get; } + + public TypedKernelAccessor(IServiceProvider serviceProvider) + { + Kernel = serviceProvider.GetKeyedService( + AbpAIOptions.GetKernelServiceKeyName( + WorkspaceNameAttribute.GetWorkspaceName())); + } +} + + diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfiguration.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfiguration.cs new file mode 100644 index 0000000000..ddbfb27b59 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfiguration.cs @@ -0,0 +1,28 @@ +using System; + +namespace Volo.Abp.AI; + +public class WorkspaceConfiguration +{ + public string Name { get; } + public ChatClientConfiguration ChatClient { get; } = new(); + public KernelConfiguration Kernel { get; } = new(); + + public WorkspaceConfiguration(string name) + { + Name = name; + } + + public WorkspaceConfiguration ConfigureChatClient(Action configureAction) + { + configureAction(ChatClient); + return this; + } + + + public WorkspaceConfiguration ConfigureKernel(Action configureAction) + { + configureAction(Kernel); + return this; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfigurationDictionary.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfigurationDictionary.cs new file mode 100644 index 0000000000..6a5c77d7d0 --- /dev/null +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/WorkspaceConfigurationDictionary.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.AI; + +public class WorkspaceConfigurationDictionary : Dictionary +{ + public void Configure(Action? configureAction = null) + where TWorkSpace : class + { + Configure(WorkspaceNameAttribute.GetWorkspaceName(), configureAction); + } + + public void Configure(string name, Action? configureAction = null) + { + if (!TryGetValue(name, out var configuration)) + { + configuration = new WorkspaceConfiguration(name); + this[name] = configuration; + } + + configureAction?.Invoke(configuration); + } + + public void ConfigureDefault(Action? configureAction = null) + { + Configure(AbpAIModule.DefaultWorkspaceName, configureAction); + } +} diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs index 632bfa416e..15a425105b 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs @@ -30,9 +30,17 @@ public class AbpCachingModule : AbpModule context.Services.AddSingleton(typeof(IHybridCache<>), typeof(AbpHybridCache<>)); context.Services.AddSingleton(typeof(IHybridCache<,>), typeof(AbpHybridCache<,>)); - context.Services.Configure(cacheOptions => + Configure(cacheOptions => { cacheOptions.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(20); }); + + if (context.Services.GetAbpHostEnvironment().IsDevelopment()) + { + Configure(options => + { + options.HideErrors = false; + }); + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LogoutCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LogoutCommand.cs index 2dcb08ea9b..58a687eea3 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LogoutCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LogoutCommand.cs @@ -21,9 +21,11 @@ public class LogoutCommand : IConsoleCommand, ITransientDependency Logger = NullLogger.Instance; } - public Task ExecuteAsync(CommandLineArgs commandLineArgs) + public async Task ExecuteAsync(CommandLineArgs commandLineArgs) { - return AuthService.LogoutAsync(); + await AuthService.LogoutAsync(); + + Logger.LogInformation("You are logged out."); } public string GetUsageInfo() diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Collections/NamedActionList.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Collections/NamedActionList.cs new file mode 100644 index 0000000000..f5ebbc2bc1 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Collections/NamedActionList.cs @@ -0,0 +1,14 @@ +using System; + +namespace Volo.Abp.AI; + +public class NamedActionList : NamedObjectList> +{ + public void Add(Action action) + { + this.Add(Guid.NewGuid().ToString("N"), action); + } + + public void Add(string name, Action action) + => this.Add(new NamedAction(name, action)); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Collections/NamedObjectList.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Collections/NamedObjectList.cs new file mode 100644 index 0000000000..df91c0afc3 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Collections/NamedObjectList.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AI; + +public class NamedObjectList : List + where T : NamedObject +{ + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/NamedAction.cs b/framework/src/Volo.Abp.Core/Volo/Abp/NamedAction.cs new file mode 100644 index 0000000000..f7bf5a7177 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/NamedAction.cs @@ -0,0 +1,14 @@ +using System; + +namespace Volo.Abp; + +public class NamedAction : NamedObject +{ + public Action Action { get; set; } + + public NamedAction(string name, Action action) + : base(name) + { + Action = Check.NotNull(action, nameof(action)); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/NamedObject.cs b/framework/src/Volo.Abp.Core/Volo/Abp/NamedObject.cs new file mode 100644 index 0000000000..579a97cce1 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/NamedObject.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp; + +public class NamedObject +{ + public string Name { get; } + + public NamedObject(string name) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs index dd5df32b8a..518de26437 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs @@ -195,13 +195,10 @@ public class DefaultExceptionToErrorInfoConverter : IExceptionToErrorInfoConvert { if (exception.EntityType != null) { - return new RemoteServiceErrorInfo( - string.Format( - L["EntityNotFoundErrorMessage"], - exception.EntityType.Name, - exception.Id - ) - ); + var message = exception.Id != null + ? string.Format(L["EntityNotFoundErrorMessage"], exception.EntityType.Name, exception.Id) + : string.Format(L["EntityNotFoundErrorMessageWithoutId"], exception.EntityType.Name); + return new RemoteServiceErrorInfo(message); } return new RemoteServiceErrorInfo(exception.Message); diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json index 07820b6503..d261f4c15c 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "المورد غير موجود!", "DefaultErrorMessage404Detail": "لم يتم العثور على المورد المطلوب على الخادم!", "EntityNotFoundErrorMessage": "لا يوجد كيان {0} بالمعرف = {1}!", + "EntityNotFoundErrorMessageWithoutId": "لا يوجد كيان {0}!", "AbpDbConcurrencyErrorMessage": "تم تغيير البيانات التي قدمتها بالفعل من قبل مستخدم/عميل آخر. يرجى تجاهل التغييرات التي قمت بها والمحاولة من البداية.", "Error": "خطأ", "UnhandledException": "استثناء غير معالج!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/cs.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/cs.json index abfc0c5d29..1bfd23ae29 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/cs.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/cs.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Prostředek nenalezen!", "DefaultErrorMessage404Detail": "Vyžádaný prostředek nebyl na serveru nalezen!", "EntityNotFoundErrorMessage": "Neexistující entita {0} s id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Neexistující entita {0}!", "AbpDbConcurrencyErrorMessage": "Údaje, které jste odeslali, již změnil jiný uživatel/klient. Zahoďte provedené změny a zkuste to od začátku.", "Error": "Chyba", "UnhandledException": "Neošetřená výjimka!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/de.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/de.json index 5930a11e43..7c50dde3fb 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/de.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/de.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Ressource nicht gefunden!", "DefaultErrorMessage404Detail": "Die angeforderte Ressource konnte nicht auf dem Server gefunden werden!", "EntityNotFoundErrorMessage": "Es gibt keine Entität {0} mit id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Es gibt keine Entität {0}!", "AbpDbConcurrencyErrorMessage": "Die von Ihnen übermittelten Daten wurden bereits von einem anderen Benutzer/Kunden geändert. Bitte verwerfen Sie die vorgenommenen Änderungen und versuchen Sie es von vorne.", "Error": "Fehler", "UnhandledException": "Unbehandelte Ausnahme!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/el.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/el.json index 00e2982c85..4a2987608f 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/el.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/el.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Ο πόρος δεν βρέθηκε!", "DefaultErrorMessage404Detail": "Ο πόρος που ζητήθηκε δεν βρέθηκε στον διακομιστή!", "EntityNotFoundErrorMessage": "Δεν υπάρχει οντότητα {0} με id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Δεν υπάρχει οντότητα {0}!", "AbpDbConcurrencyErrorMessage": "Τα δεδομένα που έχετε υποβάλλει έχουν ήδη τροποποιηθεί από άλλον χρήστη/πελάτη. Απορρίψτε τις αλλαγές που κάνατε και δοκιμάστε από την αρχή.", "Error": "Σφάλμα", "UnhandledException": "Απρόσμενη Εξαίρεση!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en-GB.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en-GB.json index d27cdd3159..7c0ccfecd7 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en-GB.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en-GB.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Resource not found!", "DefaultErrorMessage404Detail": "The resource requested could not be found on the server!", "EntityNotFoundErrorMessage": "There is no entity {0} with id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "There is no entity {0}!", "Error": "Error", "UnhandledException": "Unhandled exception!", "Authorizing": "Authorizing…", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en.json index e78b9d455b..8719e63ac2 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Resource not found!", "DefaultErrorMessage404Detail": "The resource requested could not be found on the server!", "EntityNotFoundErrorMessage": "There is no entity {0} with id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "There is no entity {0}!", "AbpDbConcurrencyErrorMessage": "The data you have submitted has already been changed by another user. Discard your changes and try again.", "Error": "Error", "UnhandledException": "Unhandled exception!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/es.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/es.json index 54913d8647..630e519626 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/es.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/es.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Recurso no encontrado!", "DefaultErrorMessage404Detail": "El recurso solitiado podría no encontrarse en el servidor!", "EntityNotFoundErrorMessage": "No hay una entidad {0} con id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "No hay una entidad {0}!", "AbpDbConcurrencyErrorMessage": "Los datos que ha enviado ya han sido modificados por otro usuario/cliente. Descarte los cambios que ha realizado e inténtelo desde el principio.", "Error": "Error", "UnhandledException": "Excepción no manejada!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fa.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fa.json index c5974f7150..21d3ab9cb9 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fa.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fa.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "منبع درخواستی یافت نشد!", "DefaultErrorMessage404Detail": "منبع درخواستی در سرور یافت نشد!", "EntityNotFoundErrorMessage": "هیچ موجودیتی {0} با id = {1} وجود ندارد!", + "EntityNotFoundErrorMessageWithoutId": "هیچ موجودیتی {0} وجود ندارد!", "AbpDbConcurrencyErrorMessage": "اطلاعات ارسالی شما قبلاً توسط کاربر/مشتری دیگری تغییر یافته است. لطفاً تغییراتی را که انجام داده اید لغو کنید و مجددا تلاش فرمایید.", "Error": "خطا", "UnhandledException": "خطای پیش بینی نشده!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fi.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fi.json index 125850a6cd..5f1b9c4d17 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fi.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fi.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Resurssia ei löydy!", "DefaultErrorMessage404Detail": "Pyydettyä resurssia ei löytynyt palvelimelta!", "EntityNotFoundErrorMessage": "Ei ole olemassa kohdetta {0}, jonka tunnus = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Ei ole olemassa kohdetta {0}!", "AbpDbConcurrencyErrorMessage": "Toinen käyttäjä/asiakas on jo muuttanut lähettämiäsi tietoja. Hylkää tekemäsi muutokset ja yritä alusta.", "Error": "Virhe", "UnhandledException": "Käsittelemätön poikkeus!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fr.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fr.json index 5349e7eed8..3d8a54506a 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fr.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fr.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Ressource introuvable!", "DefaultErrorMessage404Detail": "La ressource demandée est introuvable sur le serveur!", "EntityNotFoundErrorMessage": "Il n'y a pas d'entité {0} avec id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Il n'y a pas d'entité {0}!", "AbpDbConcurrencyErrorMessage": "Les données que vous avez soumises ont déjà été modifiées par un autre utilisateur/client. Veuillez ignorer les modifications que vous avez apportées et réessayer depuis le début.", "Error": "Erreur", "UnhandledException": "Exception non-gérée!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hi.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hi.json index f9465c3cbd..5a130a91d1 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hi.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hi.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "संसाधन नही मिला!", "DefaultErrorMessage404Detail": "अनुरोधित संसाधन सर्वर पर नहीं मिला!", "EntityNotFoundErrorMessage": "Id = {1} के साथ कोई इकाई {0} नहीं है!", + "EntityNotFoundErrorMessageWithoutId": "कोई इकाई {0} नहीं है!", "AbpDbConcurrencyErrorMessage": "आपके द्वारा सबमिट किया गया डेटा पहले ही किसी अन्य उपयोगकर्ता/क्लाइंट द्वारा बदल दिया गया है। कृपया अपने द्वारा किए गए परिवर्तनों को त्याग दें और शुरुआत से ही प्रयास करें।", "Error": "त्रुटि", "UnhandledException": "अनियंत्रित अपवाद!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hr.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hr.json index 1a494c5ae5..2ba1c24703 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hr.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hr.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Resurs nije pronađen!", "DefaultErrorMessage404Detail": "Zatraženi resurs nije pronađen na poslužitelju!", "EntityNotFoundErrorMessage": "Ne postoji entitet {0} s ID = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Ne postoji entitet {0}!", "AbpDbConcurrencyErrorMessage": "Podatke koje ste dostavili već je promijenio drugi korisnik/klijent. Odbacite promjene koje ste napravili i pokušajte ispočetka.", "Error": "Greška", "UnhandledException": "Neobrađena iznimka!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hu.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hu.json index 06a279d759..3ba9f8ffc2 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hu.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hu.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Az erőforrás nem található!", "DefaultErrorMessage404Detail": "A kért erőforrás nem található a szerveren", "EntityNotFoundErrorMessage": "Nincs {0} elem, amelynek id értéke {1}!", + "EntityNotFoundErrorMessageWithoutId": "Nincs {0} elem!", "AbpDbConcurrencyErrorMessage": "Az Ön által elküldött adatokat egy másik felhasználó/ügyfél már megváltoztatta. Kérjük, dobja el az elvégzett módosításokat, és próbálja elölről.", "Error": "Nincs {0} elem, amelynek id értéke {1}!", "UnhandledException": "Nem kezelt kivétel!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/is.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/is.json index 648693578e..839c04dded 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/is.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/is.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Auðlind fannst ekki!", "DefaultErrorMessage404Detail": "Auðlindin sem óskað var eftir fannst ekki á netþjóninum!", "EntityNotFoundErrorMessage": "Það er enginn eining {0} með id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Það er enginn eining {0}!", "AbpDbConcurrencyErrorMessage": "Gögnunum sem þú hefur sent inn hefur þegar verið breytt af öðrum notanda/viðskiptavini. Fleygðu breytingunum sem þú hefur gert og reyndu frá upphafi.", "Error": "Villa", "UnhandledException": "Ómeðhöndluð villa", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/it.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/it.json index c51c86ccd9..19d34336a5 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/it.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/it.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Risorsa non trovata!", "DefaultErrorMessage404Detail": "La risorsa richiesta non è stata trovata sul server!", "EntityNotFoundErrorMessage": "Non esiste un'entità {0} con id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Non esiste un'entità {0}!", "AbpDbConcurrencyErrorMessage": "I dati che hai inviato sono già stati modificati da un altro utente/cliente. Per favore scarta le modifiche che hai fatto e riprova dall'inizio.", "Error": "Errore", "UnhandledException": "Eccezione non gestita!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/nl.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/nl.json index 0d91830578..e7dc05d386 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/nl.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/nl.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Bron niet gevonden!", "DefaultErrorMessage404Detail": "De gevraagde bron kan niet worden gevonden op de server!", "EntityNotFoundErrorMessage": "Er is geen entiteit {0} met id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Er is geen entiteit {0}!", "AbpDbConcurrencyErrorMessage": "De door u ingevulde gegevens zijn al gewijzigd door een andere gebruiker/klant. Negeer de wijzigingen die u heeft aangebracht en probeer het vanaf het begin.", "Error": "Fout", "UnhandledException": "Onverwerkte uitzondering!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pl-PL.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pl-PL.json index f8a6b1e126..502d9b4ab9 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pl-PL.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pl-PL.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Nie znaleziono zasobu!", "DefaultErrorMessage404Detail": "Nie znaleziono zasobu z żądania na serwerze!", "EntityNotFoundErrorMessage": "Nie istnieje encja {0} z id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Nie istnieje encja {0}!", "AbpDbConcurrencyErrorMessage": "Przesłane przez Ciebie dane zostały już zmienione przez innego użytkownika/klienta. Odrzuć wprowadzone zmiany i spróbuj od początku.", "Error": "Błąd", "UnhandledException": "Nieobsługiwany wyjątek!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pt-BR.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pt-BR.json index ccc5be09b3..4d88b28954 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pt-BR.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pt-BR.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Recurso não encontrado!", "DefaultErrorMessage404Detail": "O recurso requisitado não pode ser encontrado pelo servidor!", "EntityNotFoundErrorMessage": "Não existe uma entidade {0} com código = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Não existe uma entidade {0}!", "AbpDbConcurrencyErrorMessage": "Os dados que você enviou já foram alterados por outro usuário/cliente. Descarte as alterações feitas e tente desde o início.", "Error": "Erro", "UnhandledException": "Exceção não tratada!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ro-RO.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ro-RO.json index 4c9ba8602b..ea74f93945 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ro-RO.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ro-RO.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Resursa nu a fost găsită!", "DefaultErrorMessage404Detail": "Resursa solicitată nu a fost găsită pe server!", "EntityNotFoundErrorMessage": "Nu există entitatea {0} cu id-ul {1}!", + "EntityNotFoundErrorMessageWithoutId": "Nu există entitatea {0}!", "AbpDbConcurrencyErrorMessage": "Datele pe care le-aţi trimis au fost modificate deja de către alt utilizator/client. Vă rugăm să renunţaţi la modificările pe care le-aţi făcut şi să încercaţi de la început.", "Error": "Eroare", "UnhandledException": "Excepţie netratată!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ru.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ru.json index 39cc06cb69..aceab939af 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ru.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ru.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Ресурс не найден!", "DefaultErrorMessage404Detail": "Запрошенный ресурс не удалось найти на сервере!", "EntityNotFoundErrorMessage": "Нет объекта {0} с id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Нет объекта {0}!", "AbpDbConcurrencyErrorMessage": "Отправленные вами данные уже были изменены другим пользователем/клиентом. Отмените внесенные вами изменения и попробуйте с самого начала.", "Error": "Ошибка", "UnhandledException": "Непредвиденная ошибка!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sk.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sk.json index b5d39f33ee..5aba707b33 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sk.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sk.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Zdroj nebol nájdený!", "DefaultErrorMessage404Detail": "Požadovaný zdroj sa na serveri nenašiel!", "EntityNotFoundErrorMessage": "Entita {0} s id = {1} neexistuje!", + "EntityNotFoundErrorMessageWithoutId": "Entita {0} neexistuje!", "AbpDbConcurrencyErrorMessage": "Údaje, ktoré ste odoslali, už zmenil iný používateľ/klient. Zahoďte zmeny, ktoré ste vykonali, a skúste to od začiatku.", "Error": "Error", "UnhandledException": "Neošetrená výnimka!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sl.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sl.json index ea8b52bc23..fe4185dd4f 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sl.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sl.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Vir ni bil najden!", "DefaultErrorMessage404Detail": "Zahtevanega vira ni bilo mogoče najti na strežniku!", "EntityNotFoundErrorMessage": "Ni entitete {0} z id-jem = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Ni entitete {0}!", "AbpDbConcurrencyErrorMessage": "Podatke, ki ste jih poslali, je že spremenil drug uporabnik/stranka. Zavrzite spremembe, ki ste jih naredili, in poskusite od začetka.", "Error": "Napaka", "UnhandledException": "Neobravnavana napaka!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sv.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sv.json index a7bfc97f4e..daa774c11d 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sv.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sv.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Resurs hittades inte!", "DefaultErrorMessage404Detail": "Den begärda resursen kunde inte hittas på servern!", "EntityNotFoundErrorMessage": "Det finns ingen entitet {0} med id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Det finns ingen entitet {0}!", "AbpDbConcurrencyErrorMessage": "De uppgifter du har skickat har redan ändrats av en annan användare. Kassera dina ändringar och försök igen.", "Error": "Fel", "UnhandledException": "Obehandlat undantag!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/tr.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/tr.json index 4429ff7bfd..5dea2ccb52 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/tr.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/tr.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Kaynak bulunamadı!", "DefaultErrorMessage404Detail": "İstenilen kaynak sunucuda bulunamadı.", "EntityNotFoundErrorMessage": "Id değeri {1} olan {0} türünden bir nesne bulunamadı!", + "EntityNotFoundErrorMessageWithoutId": "{0} türünden bir nesne bulunamadı!", "AbpDbConcurrencyErrorMessage": "Gönderdiğiniz veri başka bir kullanıcı/istemci tarafından değiştirilmiş. Lütfen işlemi iptal edip baştan deneyin.", "Error": "Hata", "UnhandledException": "Yakalanmamış hata!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/vi.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/vi.json index 230f005501..9e7d06809c 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/vi.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/vi.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "Tài nguyên không tìm thấy!", "DefaultErrorMessage404Detail": "Tài nguyên được yêu cầu không được tìm thấy trên máy chủ!", "EntityNotFoundErrorMessage": "Không có thực thể nào {0} với id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "Không có thực thể nào {0}!", "AbpDbConcurrencyErrorMessage": "Dữ liệu bạn gửi đã bị người dùng/khách hàng khác thay đổi. Vui lòng hủy các thay đổi bạn đã thực hiện và thử lại từ đầu.", "Error": "Lỗi", "UnhandledException": "Tình huống ngoại lệ không thể xử lí được!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json index 697358fe36..8f0361d1eb 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "未找到资源!", "DefaultErrorMessage404Detail": "服务器上找不到所请求的资源!", "EntityNotFoundErrorMessage": "不存在 id = {1} 的实体 {0}!", + "EntityNotFoundErrorMessageWithoutId": "不存在实体 {0}!", "AbpDbConcurrencyErrorMessage": "您提交的数据已被其他用户/客户更改。请放弃您所做的更改并从头开始尝试。", "Error": "错误", "UnhandledException": "未处理异常!", diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hant.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hant.json index 2a0cb25d76..5c62432e56 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hant.json +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hant.json @@ -13,6 +13,7 @@ "DefaultErrorMessage404": "未找到資源!", "DefaultErrorMessage404Detail": "未在服務中找到請求的資源!", "EntityNotFoundErrorMessage": "實體 {0} 不存在,id = {1}!", + "EntityNotFoundErrorMessageWithoutId": "實體 {0} 不存在!", "AbpDbConcurrencyErrorMessage": "你提交的數據已經被其他用戶/客戶端修改.請放棄你所做的修改並再次嘗試.", "Error": "錯誤", "UnhandledException": "未處理的異常!", diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs index 59a0085d4c..d61738afb2 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs @@ -42,12 +42,12 @@ public class FeatureManagementStore : IFeatureManagementStore, ITransientDepende if (featureValue == null) { featureValue = new FeatureValue(GuidGenerator.Create(), name, value, providerName, providerKey); - await FeatureValueRepository.InsertAsync(featureValue); + await FeatureValueRepository.InsertAsync(featureValue, true); } else { featureValue.Value = value; - await FeatureValueRepository.UpdateAsync(featureValue); + await FeatureValueRepository.UpdateAsync(featureValue, true); } await Cache.SetAsync(CalculateCacheKey(name, providerName, providerKey), new FeatureValueCacheItem(featureValue?.Value), considerUow: true); @@ -59,7 +59,7 @@ public class FeatureManagementStore : IFeatureManagementStore, ITransientDepende var featureValues = await FeatureValueRepository.FindAllAsync(name, providerName, providerKey); foreach (var featureValue in featureValues) { - await FeatureValueRepository.DeleteAsync(featureValue); + await FeatureValueRepository.DeleteAsync(featureValue, true); await Cache.RemoveAsync(CalculateCacheKey(name, providerName, providerKey), considerUow: true); } } diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs index 4174eb95b0..bec82eaada 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs @@ -135,8 +135,6 @@ public abstract class FeatureManagementStore_Tests : FeatureMana EditionFeatureValueProvider.ProviderName, TestEditionIds.Regular.ToString()); - await uow.SaveChangesAsync(); - // Assert (await FeatureManagementStore.GetOrNullAsync(TestFeatureDefinitionProvider.SocialLogins, EditionFeatureValueProvider.ProviderName, diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs index b6dfc2e518..e853377ddb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs @@ -70,15 +70,8 @@ public abstract class PermissionManagementProvider : IPermissionManagementProvid return; } - await PermissionGrantRepository.InsertAsync( - new PermissionGrant( - GuidGenerator.Create(), - name, - Name, - providerKey, - CurrentTenant.Id - ) - ); + permissionGrant = new PermissionGrant(GuidGenerator.Create(), name, Name, providerKey, CurrentTenant.Id); + await PermissionGrantRepository.InsertAsync(permissionGrant, true); } protected virtual async Task RevokeAsync(string name, string providerKey) @@ -89,6 +82,6 @@ public abstract class PermissionManagementProvider : IPermissionManagementProvid return; } - await PermissionGrantRepository.DeleteAsync(permissionGrant); + await PermissionGrantRepository.DeleteAsync(permissionGrant, true); } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs index b664e5c92d..8eeea26a97 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs @@ -184,7 +184,7 @@ public class PermissionManager : IPermissionManager, ISingletonDependency } permissionGrant.ProviderKey = providerKey; - return await PermissionGrantRepository.UpdateAsync(permissionGrant); + return await PermissionGrantRepository.UpdateAsync(permissionGrant, true); } public virtual async Task DeleteAsync(string providerName, string providerKey) @@ -192,7 +192,7 @@ public class PermissionManager : IPermissionManager, ISingletonDependency var permissionGrants = await PermissionGrantRepository.GetListAsync(providerName, providerKey); foreach (var permissionGrant in permissionGrants) { - await PermissionGrantRepository.DeleteAsync(permissionGrant); + await PermissionGrantRepository.DeleteAsync(permissionGrant, true); } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManager_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManager_Tests.cs index 3f2e9c7565..b851c0daa9 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManager_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManager_Tests.cs @@ -134,7 +134,7 @@ public class PermissionManager_Tests : PermissionTestBase "Test", true); } - + [Fact] public async Task Set_Should_Throw_Exception_If_Provider_Not_Found() { @@ -146,7 +146,7 @@ public class PermissionManager_Tests : PermissionTestBase "Test", true); }); - + exception.Message.ShouldBe("Unknown permission management provider: UndefinedProvider"); } @@ -165,4 +165,20 @@ public class PermissionManager_Tests : PermissionTestBase await _permissionManager.UpdateProviderKeyAsync(permissionGrant, "NewProviderKey"); (await _permissionGrantRepository.FindAsync("MyPermission1", "Test", "NewProviderKey")).ShouldNotBeNull(); } + + [Fact] + public async Task DeleteAsync() + { + await _permissionGrantRepository.InsertAsync(new PermissionGrant( + Guid.NewGuid(), + "MyPermission1", + "Test", + "Test") + ); + var permissionGrant = await _permissionGrantRepository.FindAsync("MyPermission1", "Test", "Test"); + permissionGrant.ProviderKey.ShouldBe("Test"); + + await _permissionManager.DeleteAsync("Test","Test"); + (await _permissionGrantRepository.FindAsync("MyPermission1", "Test", "Test")).ShouldBeNull(); + } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs index 6f11da24e5..09f90c8919 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs @@ -41,12 +41,12 @@ public class SettingManagementStore : ISettingManagementStore, ITransientDepende if (setting == null) { setting = new Setting(GuidGenerator.Create(), name, value, providerName, providerKey); - await SettingRepository.InsertAsync(setting); + await SettingRepository.InsertAsync(setting, true); } else { setting.Value = value; - await SettingRepository.UpdateAsync(setting); + await SettingRepository.UpdateAsync(setting, true); } await Cache.SetAsync(CalculateCacheKey(name, providerName, providerKey), new SettingCacheItem(setting?.Value), considerUow: true); @@ -64,7 +64,7 @@ public class SettingManagementStore : ISettingManagementStore, ITransientDepende var setting = await SettingRepository.FindAsync(name, providerName, providerKey); if (setting != null) { - await SettingRepository.DeleteAsync(setting); + await SettingRepository.DeleteAsync(setting, true); await Cache.RemoveAsync(CalculateCacheKey(name, providerName, providerKey), considerUow: true); } } diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs index 23acc4e718..7e375e2b42 100644 --- a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs +++ b/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs @@ -66,6 +66,14 @@ public class SettingManagementStore_Tests : SettingsTestBase var valueAfterSet = await _settingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); valueAfterSet.ShouldBe("43"); + + await _settingManagementStore.DeleteAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); + + var values = await _settingManagementStore.GetListAsync(["MySetting1"], GlobalSettingValueProvider.ProviderName, null); + + var settingValue = values.FirstOrDefault(x => x.Name == "MySetting1"); + settingValue.ShouldNotBeNull(); + settingValue.Value.ShouldBeNull(); } } diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index 018d62d786..20b08ac2ac 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -93,6 +93,8 @@ $solutions = ( $projects = ( # framework + "framework/src/Volo.Abp.AI.Abstractions", + "framework/src/Volo.Abp.AI", "framework/src/Volo.Abp.ApiVersioning.Abstractions", "framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer", "framework/src/Volo.Abp.AspNetCore.Authentication.OAuth",