Resolve "Custom Repositories" #72
46
.idea/.idea.HopFrame/.idea/workspace.xml
generated
46
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -11,10 +11,7 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Fixed directory in pipeline">
|
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="" />
|
||||||
<change beforePath="$PROJECT_DIR$/.gitlab-ci.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.gitlab-ci.yml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
@@ -33,7 +30,7 @@
|
|||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="feature/exporters" />
|
<entry key="$PROJECT_DIR$" value="dev" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
@@ -62,6 +59,7 @@
|
|||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/a0/0a968c53/IEnumerable`1.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/a0/0a968c53/IEnumerable`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/ad/ba9a50e7/ICollection.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/ad/ba9a50e7/ICollection.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/c73b3c6c598640c592fd3c6fa226c286e90908/fc/6f7933d2/ICollection`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/0f73968de5cdfe0aa57817b8dd2a3c5d1db615ba4ae4629a5af59bb6c8922/RemoteNavigationManager.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10c66a9a1e137111895f7182a2ae246eabe06a261578c3fa495a45f6f177d35/IconVariant.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10c66a9a1e137111895f7182a2ae246eabe06a261578c3fa495a45f6f177d35/IconVariant.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/1b81cb3be224213a6a73519b6e340a628d9a1fb8629c351a186a26f6376669/List.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/1b81cb3be224213a6a73519b6e340a628d9a1fb8629c351a186a26f6376669/List.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/24dd1164ba47541cb1d3eb011e638e16953dbea3ae3f4dc208c3bbf3e96298a/ServiceCollectionServiceExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/24dd1164ba47541cb1d3eb011e638e16953dbea3ae3f4dc208c3bbf3e96298a/ServiceCollectionServiceExtensions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
@@ -79,6 +77,7 @@
|
|||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6d1d64f05e7045295fa180276a8c2aef0302c9e96eb53b3431ab13db4579/FluentAppBarItem.razor.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6d1d64f05e7045295fa180276a8c2aef0302c9e96eb53b3431ab13db4579/FluentAppBarItem.razor.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6fe785cceb29ca2d1da78e157315815a7c4372b582a20a71c28b210f9d56e/IconsExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6fe785cceb29ca2d1da78e157315815a7c4372b582a20a71c28b210f9d56e/IconsExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7ad7d2d0ae865063993eb8a03427815ea3bdb6a774e0a2f95512e9f669a4f489/MemberEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/7ad7d2d0ae865063993eb8a03427815ea3bdb6a774e0a2f95512e9f669a4f489/MemberEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/876cd892fc66a9dc8f6afd3704c264acebdfc46aed08089463e8117c21a532/String.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/87c584767b46b5fd42769be76547105558e6690f785614efddca134b2d682/Type.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/87c584767b46b5fd42769be76547105558e6690f785614efddca134b2d682/Type.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/8d5d6cbff46ddc7b152381f92ae1ae51d3e7b57b14dd23840a11f5aaaaed396/InternalEntityEntry.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a882d183338544fdbcbdfc7b6d3dcb78916630765551644a221b5be9c45a121b/Int32.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/a882d183338544fdbcbdfc7b6d3dcb78916630765551644a221b5be9c45a121b/Int32.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
@@ -129,7 +128,7 @@
|
|||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
"b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
|
||||||
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
"dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
|
||||||
"git-widget-placeholder": "dev",
|
"git-widget-placeholder": "!34 on feature/repositories",
|
||||||
"list.type.of.created.stylesheet": "CSS",
|
"list.type.of.created.stylesheet": "CSS",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
@@ -258,15 +257,8 @@
|
|||||||
<workItem from="1740738257628" duration="3216000" />
|
<workItem from="1740738257628" duration="3216000" />
|
||||||
<workItem from="1740741585276" duration="17000" />
|
<workItem from="1740741585276" duration="17000" />
|
||||||
<workItem from="1740742098571" duration="78000" />
|
<workItem from="1740742098571" duration="78000" />
|
||||||
<workItem from="1740742471317" duration="413000" />
|
<workItem from="1740742471317" duration="672000" />
|
||||||
</task>
|
<workItem from="1741974241977" duration="10854000" />
|
||||||
<task id="LOCAL-00001" summary="Added basic configuration">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1736850899254</created>
|
|
||||||
<option name="number" value="00001" />
|
|
||||||
<option name="presentableId" value="LOCAL-00001" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1736850899254</updated>
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00002" summary="Added admin page navigation">
|
<task id="LOCAL-00002" summary="Added admin page navigation">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -644,7 +636,23 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1740742749325</updated>
|
<updated>1740742749325</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="49" />
|
<task id="LOCAL-00049" summary="Reverted pipeline to include all jobs">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1740743139064</created>
|
||||||
|
<option name="number" value="00049" />
|
||||||
|
<option name="presentableId" value="LOCAL-00049" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1740743139064</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00050" summary="Added support for custom repositories">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1741985203179</created>
|
||||||
|
<option name="number" value="00050" />
|
||||||
|
<option name="presentableId" value="LOCAL-00050" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1741985203179</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="51" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -695,8 +703,6 @@
|
|||||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||||
<MESSAGE value="Added a simple web api abstraction method" />
|
|
||||||
<MESSAGE value="Implemented async delegates" />
|
|
||||||
<MESSAGE value="Added maximum display length" />
|
<MESSAGE value="Added maximum display length" />
|
||||||
<MESSAGE value="Fixed test for table view" />
|
<MESSAGE value="Fixed test for table view" />
|
||||||
<MESSAGE value="Added n-m relation mapping" />
|
<MESSAGE value="Added n-m relation mapping" />
|
||||||
@@ -720,6 +726,8 @@
|
|||||||
<MESSAGE value="Prepared CI for v3.2.0" />
|
<MESSAGE value="Prepared CI for v3.2.0" />
|
||||||
<MESSAGE value="Removed unused dependency" />
|
<MESSAGE value="Removed unused dependency" />
|
||||||
<MESSAGE value="Fixed directory in pipeline" />
|
<MESSAGE value="Fixed directory in pipeline" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Fixed directory in pipeline" />
|
<MESSAGE value="Reverted pipeline to include all jobs" />
|
||||||
|
<MESSAGE value="Added support for custom repositories" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Added support for custom repositories" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<toc-element topic="PropertyConfig.md"/>
|
<toc-element topic="PropertyConfig.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element topic="Callbacks.md"/>
|
<toc-element topic="Callbacks.md"/>
|
||||||
|
<toc-element topic="Custom-Repositories.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element toc-title="Web Module">
|
<toc-element toc-title="Web Module">
|
||||||
<toc-element toc-title="Interface">
|
<toc-element toc-title="Interface">
|
||||||
@@ -27,6 +28,10 @@
|
|||||||
<toc-element topic="Plugins.md">
|
<toc-element topic="Plugins.md">
|
||||||
<toc-element topic="Events.md">
|
<toc-element topic="Events.md">
|
||||||
</toc-element>
|
</toc-element>
|
||||||
|
<toc-element topic="Exporter-Plugin.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
|
<toc-element toc-title="Services">
|
||||||
|
<toc-element topic="IFileService.md"/>
|
||||||
|
</toc-element>
|
||||||
</instance-profile>
|
</instance-profile>
|
||||||
132
docs/Writerside/topics/Custom-Repositories.md
Normal file
132
docs/Writerside/topics/Custom-Repositories.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Custom Repositories
|
||||||
|
|
||||||
|
Custom repositories in HopFrame allow you to define and integrate custom logic for managing database entities. By implementing the `IHopFrameRepository<TModel, TKey>` interface, you can gain full control over how data is retrieved, modified, and managed. This feature is ideal for scenarios where the default behavior does not meet specific business requirements.
|
||||||
|
|
||||||
|
## IHopFrameRepository<TModel, TKey> Interface
|
||||||
|
|
||||||
|
The `IHopFrameRepository<TModel, TKey>` interface defines a contract for a repository that works with a specific model (`TModel`) and its primary key (`TKey`). The interface provides the following methods:
|
||||||
|
|
||||||
|
- **LoadPage**
|
||||||
|
Loads a paginated set of items.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task<IEnumerable<TModel>> LoadPage(int page, int perPage);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `page`: The page number to load.
|
||||||
|
- `perPage`: The number of items per page.
|
||||||
|
- **Returns:** A collection of items for the specified page.
|
||||||
|
|
||||||
|
- **Search**
|
||||||
|
Performs a search query on the repository.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task<SearchResult<TModel>> Search(string searchTerm, int page, int perPage);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `searchTerm`: The term to search for.
|
||||||
|
- `page`: The page number to load.
|
||||||
|
- `perPage`: The number of items per page.
|
||||||
|
- **Returns:** A `SearchResult` containing matching items and the total number of pages.
|
||||||
|
|
||||||
|
- **GetTotalPageCount**
|
||||||
|
Retrieves the total number of pages based on the items per page.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task<int> GetTotalPageCount(int perPage);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `perPage`: The number of items per page.
|
||||||
|
- **Returns:** The total number of pages.
|
||||||
|
|
||||||
|
- **CreateItem**
|
||||||
|
Adds a new item to the repository.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task CreateItem(TModel item);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `item`: The item to create.
|
||||||
|
|
||||||
|
- **EditItem**
|
||||||
|
Updates an existing item in the repository.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task EditItem(TModel item);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `item`: The item to update.
|
||||||
|
|
||||||
|
- **DeleteItem**
|
||||||
|
Removes an item from the repository.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task DeleteItem(TModel item);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `item`: The item to delete.
|
||||||
|
|
||||||
|
- **GetOne**
|
||||||
|
Retrieves a single item based on its primary key.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task<TModel?> GetOne(TKey key);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `key`: The primary key of the item to retrieve.
|
||||||
|
- **Returns:** The item if found, or `null` if not.
|
||||||
|
|
||||||
|
## `SearchResult<TModel>` Struct
|
||||||
|
|
||||||
|
The `SearchResult<TModel>` struct is used to encapsulate the results of a search query.
|
||||||
|
|
||||||
|
- **Properties:**
|
||||||
|
- `Items`: The items retrieved from the search query.
|
||||||
|
- `PageCount`: The total number of pages based on the search results.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
public readonly struct SearchResult<TModel>(IEnumerable<TModel> items, int pageCount) {
|
||||||
|
public IEnumerable<TModel> Items { get; init; }
|
||||||
|
public int PageCount { get; init; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Custom Repositories
|
||||||
|
|
||||||
|
To add and configure a custom repository in HopFrame, use the `AddCustomRepository` methods. These methods allow you to specify a repository class (`TRepository`) implementing `IHopFrameRepository<TModel, TKey>` and define configurations for the associated table.
|
||||||
|
|
||||||
|
- **With Configurator**
|
||||||
|
|
||||||
|
```c#
|
||||||
|
HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(
|
||||||
|
Expression<Func<TModel, TKey>> keyExpression,
|
||||||
|
Action<TableConfigurator<TModel>> configurator
|
||||||
|
)
|
||||||
|
where TRepository : IHopFrameRepository<TModel, TKey>;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `keyExpression`: The key of the model.
|
||||||
|
- `configurator`: Configures the table page.
|
||||||
|
|
||||||
|
- **Without Configurator**
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(
|
||||||
|
Expression<Func<TModel, TKey>> keyExpression
|
||||||
|
)
|
||||||
|
where TRepository : IHopFrameRepository<TModel, TKey>;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `keyExpression`: The key of the model.
|
||||||
|
- **Returns:** A `TableConfigurator` to configure the table.
|
||||||
|
|
||||||
|
By implementing custom repositories and using these methods, you can fully leverage the flexibility of HopFrame for your data management needs. Let me know if you'd like further elaboration!
|
||||||
51
docs/Writerside/topics/Exporter-Plugin.md
Normal file
51
docs/Writerside/topics/Exporter-Plugin.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Exporter Plugin
|
||||||
|
|
||||||
|
The Exporter Plugin is a tool for managing the import and export of data from the HopFrame UI. It provides functionality for exporting table data into a CSV file and importing data back into the system, making data manipulation and backups more seamless.
|
||||||
|
|
||||||
|
## What the Exporter Plugin Does
|
||||||
|
|
||||||
|
1. **Export Table Data to CSV**
|
||||||
|
- The plugin allows users to export all data from a table as a CSV file.
|
||||||
|
- The exported file includes all non-virtual properties as table headers.
|
||||||
|
- The export process dynamically constructs rows for each entry in the table.
|
||||||
|
|
||||||
|
2. **Import Data from CSV**
|
||||||
|
- Users can import a CSV file to populate or update a table.
|
||||||
|
- The import process reads the file, validates the headers, and creates new entries or updates existing ones.
|
||||||
|
- Relationships and enumerable properties are also resolved using the appropriate managers.
|
||||||
|
|
||||||
|
3. **User Interface Integration**
|
||||||
|
- Adds two buttons, "Export" and "Import," to the page header of each table.
|
||||||
|
- **Export Button:** Initiates the export functionality.
|
||||||
|
- **Import Button:** Allows users to upload a CSV file for import.
|
||||||
|
|
||||||
|
4. **Error Handling**
|
||||||
|
- Ensures errors during import or export (e.g., invalid file format, missing data, or system issues) are shown to the user as toast messages.
|
||||||
|
|
||||||
|
## Adding the Exporter Plugin
|
||||||
|
|
||||||
|
To include the Exporter Plugin in your HopFrame setup, use the `AddExporters` method provided by the `HopFrameConfiguratorExtensions`.
|
||||||
|
|
||||||
|
Here’s how to register the Exporter Plugin in your application configuration:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
builder.Services.AddHopFrame(options => {
|
||||||
|
options.AddExporters();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AddExporters` method internally registers the `ExporterPlugin` and attaches its functionality to the HopFrame.
|
||||||
|
|
||||||
|
## Key Features of the Export Process
|
||||||
|
|
||||||
|
- **Dynamic Header Creation:** Automatically generates headers based on the table's non-virtual properties.
|
||||||
|
- **Data Transformation:** Transforms property values into CSV-compatible formats.
|
||||||
|
- **File Download:** Saves the generated CSV file with the table’s display name.
|
||||||
|
|
||||||
|
## Key Features of the Import Process
|
||||||
|
|
||||||
|
- **Header Validation:** Validates that the CSV file headers match the table's properties.
|
||||||
|
- **Type Conversion:** Converts values in the CSV file to their respective data types.
|
||||||
|
- **Relationship Management:** Resolves relationships and enumerable properties during import.
|
||||||
|
|
||||||
|
This plugin streamlines data operations, reducing manual effort and enabling quick data migration or updates. Let me know if you’d like to dive deeper into any specific aspect!
|
||||||
@@ -118,6 +118,50 @@ DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext :
|
|||||||
|
|
||||||
- **Returns:** The configurator of the context if it already was defined, `null` if not.
|
- **Returns:** The configurator of the context if it already was defined, `null` if not.
|
||||||
|
|
||||||
|
### AddCustomRepository (With configurator)
|
||||||
|
|
||||||
|
Adds a table of the desired type and configures it to use a custom repository.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(
|
||||||
|
Expression<Func<TModel, TKey>> keyExpression,
|
||||||
|
Action<TableConfigurator<TModel>> configurator
|
||||||
|
)
|
||||||
|
where TRepository : IHopFrameRepository<TModel, TKey>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TRepository`: The repository class that inherits from `IHopFrameRepository<TModel, TKey>` (needs to be registered as a service).
|
||||||
|
- `TModel`: The model of the table.
|
||||||
|
- `TKey`: The type of the primary key.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `keyExpression`: The key of the model.
|
||||||
|
- `configurator`: The configurator used for configuring the table page.
|
||||||
|
|
||||||
|
- **Returns:** `HopFrameConfigurator`
|
||||||
|
|
||||||
|
### AddCustomRepository (Without configurator)
|
||||||
|
|
||||||
|
Adds a table of the desired type and configures it to use a custom repository.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(
|
||||||
|
Expression<Func<TModel, TKey>> keyExpression
|
||||||
|
)
|
||||||
|
where TRepository : IHopFrameRepository<TModel, TKey>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Type Parameters:**
|
||||||
|
- `TRepository`: The repository class that inherits from `IHopFrameRepository<TModel, TKey>` (needs to be registered as a service).
|
||||||
|
- `TModel`: The model of the table.
|
||||||
|
- `TKey`: The type of the primary key.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `keyExpression`: The key of the model.
|
||||||
|
|
||||||
|
- **Returns:** The configurator used for configuring the table page: `TableConfigurator<TModel>`.
|
||||||
|
|
||||||
### DisplayUserInfo
|
### DisplayUserInfo
|
||||||
|
|
||||||
Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin UI.
|
Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin UI.
|
||||||
|
|||||||
41
docs/Writerside/topics/IFileService.md
Normal file
41
docs/Writerside/topics/IFileService.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# IFileService
|
||||||
|
|
||||||
|
The `IFileService` interface provides methods for handling file operations, such as downloading and uploading files within the HopFrame web application. It abstracts file-related operations to ensure a smooth and consistent user experience.
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
1. **DownloadFile**
|
||||||
|
- Initiates the download of a file with the given name and data.
|
||||||
|
- Suitable for dynamically generating and offering files to the user, such as CSV exports or reports.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task DownloadFile(string name, byte[] data);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `name`: The name of the file to be downloaded (including the extension, e.g., "example.csv").
|
||||||
|
- `data`: The byte array representing the content of the file.
|
||||||
|
- **Usage Example:** Exporting table data as a CSV file for download.
|
||||||
|
|
||||||
|
2. **UploadFile**
|
||||||
|
- Allows the user to upload a file through the web interface and returns the uploaded file for further processing.
|
||||||
|
- This method provides integration with Blazor's `IBrowserFile` for easy file handling.
|
||||||
|
|
||||||
|
```c#
|
||||||
|
Task<IBrowserFile> UploadFile();
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Returns:** An `IBrowserFile` instance representing the uploaded file.
|
||||||
|
- **Usage Example:** Importing data from a CSV file to populate or update a table.
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
The `IFileService` is commonly used in conjunction with plugins or components that require file operations, such as the Exporter Plugin, which leverages this service to enable data export and import functionality.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- Streamlines file handling for web applications.
|
||||||
|
- Simplifies both download and upload processes with minimal code.
|
||||||
|
- Ensures compatibility with Blazor's file-handling capabilities.
|
||||||
|
|
||||||
|
By implementing or extending the `IFileService`, developers can customize the file-handling behavior to suit specific application needs. Let me know if you'd like more examples or details!
|
||||||
@@ -2,7 +2,13 @@
|
|||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
|
|
||||||
public class DbContextConfig {
|
public interface ITableGroupConfig {
|
||||||
|
public Type ContextType { get; }
|
||||||
|
public List<TableConfig> Tables { get; }
|
||||||
|
public HopFrameConfig ParentConfig { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DbContextConfig : ITableGroupConfig {
|
||||||
public Type ContextType { get; }
|
public Type ContextType { get; }
|
||||||
public List<TableConfig> Tables { get; } = new();
|
public List<TableConfig> Tables { get; } = new();
|
||||||
public HopFrameConfig ParentConfig { get; }
|
public HopFrameConfig ParentConfig { get; }
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using HopFrame.Core.Callbacks;
|
using System.Linq.Expressions;
|
||||||
|
using HopFrame.Core.Callbacks;
|
||||||
|
using HopFrame.Core.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
|
|
||||||
public class HopFrameConfig {
|
public class HopFrameConfig {
|
||||||
public List<DbContextConfig> Contexts { get; } = new();
|
public List<ITableGroupConfig> Contexts { get; } = new();
|
||||||
public bool DisplayUserInfo { get; set; } = true;
|
public bool DisplayUserInfo { get; set; } = true;
|
||||||
public string? BasePolicy { get; set; }
|
public string? BasePolicy { get; set; }
|
||||||
public string? LoginPageRewrite { get; set; }
|
public string? LoginPageRewrite { get; set; }
|
||||||
@@ -48,6 +50,36 @@ public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollecti
|
|||||||
return new DbContextConfigurator<TDbContext>(context);
|
return new DbContextConfigurator<TDbContext>(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a table of the desired type and configures it to use a custom repository
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyExpression">The key of the model</param>
|
||||||
|
/// <param name="configurator">The configurator used for configuring the table page</param>
|
||||||
|
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||||
|
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||||
|
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||||
|
public HopFrameConfigurator AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression, Action<TableConfigurator<TModel>> configurator) {
|
||||||
|
var context = AddCustomRepository<TRepository, TModel, TKey>(keyExpression);
|
||||||
|
configurator.Invoke(context);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a table of the desired type and configures it to use a custom repository
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyExpression">The key of the model</param>
|
||||||
|
/// <typeparam name="TRepository">The repository class that inherits from the <see cref="IHopFrameRepository{TModel,TKey}"/> (needs to be registered as a service)</typeparam>
|
||||||
|
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||||
|
/// <typeparam name="TKey">The type of the primary key</typeparam>
|
||||||
|
/// <returns>The configurator used for configuring the table page</returns>
|
||||||
|
public TableConfigurator<TModel> AddCustomRepository<TRepository, TModel, TKey>(Expression<Func<TModel, TKey>> keyExpression) {
|
||||||
|
var keyProperty = TableConfigurator<TModel>.GetPropertyInfo(keyExpression);
|
||||||
|
var context = new RepositoryGroupConfig(typeof(TRepository), keyProperty, InnerConfig);
|
||||||
|
context.Tables.Add(new TableConfig(context, typeof(TModel), typeof(TRepository).Name, 0));
|
||||||
|
InnerConfig.Contexts.Add(context);
|
||||||
|
return new TableConfigurator<TModel>(context.Tables[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if a context is already registered in the HopFrame
|
/// Check if a context is already registered in the HopFrame
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -64,6 +96,7 @@ public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollecti
|
|||||||
/// <returns>The configurator of the context if it already was defined, null if not</returns>
|
/// <returns>The configurator of the context if it already was defined, null if not</returns>
|
||||||
public DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext {
|
public DbContextConfigurator<TDbContext>? GetDbContext<TDbContext>() where TDbContext : DbContext {
|
||||||
var config = InnerConfig.Contexts
|
var config = InnerConfig.Contexts
|
||||||
|
.OfType<DbContextConfig>()
|
||||||
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
.SingleOrDefault(context => context.ContextType == typeof(TDbContext));
|
||||||
if (config is null) return null;
|
if (config is null) return null;
|
||||||
|
|
||||||
|
|||||||
13
src/HopFrame.Core/Config/RepositoryGroupConfig.cs
Normal file
13
src/HopFrame.Core/Config/RepositoryGroupConfig.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.Config;
|
||||||
|
|
||||||
|
public class RepositoryGroupConfig(Type repoType, PropertyInfo keyProperty, HopFrameConfig config) : ITableGroupConfig {
|
||||||
|
public Type ContextType { get; } = repoType;
|
||||||
|
|
||||||
|
public List<TableConfig> Tables { get; } = new();
|
||||||
|
|
||||||
|
public HopFrameConfig ParentConfig { get; } = config;
|
||||||
|
|
||||||
|
public PropertyInfo KeyProperty { get; } = keyProperty;
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ public class TableConfig {
|
|||||||
public string PropertyName { get; }
|
public string PropertyName { get; }
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public DbContextConfig ContextConfig { get; }
|
public ITableGroupConfig ContextConfig { get; }
|
||||||
public bool Ignored { get; set; }
|
public bool Ignored { get; set; }
|
||||||
public int Order { get; set; }
|
public int Order { get; set; }
|
||||||
internal bool Seeded { get; set; }
|
internal bool Seeded { get; set; }
|
||||||
@@ -23,7 +23,7 @@ public class TableConfig {
|
|||||||
|
|
||||||
public List<PropertyConfig> Properties { get; } = new();
|
public List<PropertyConfig> Properties { get; } = new();
|
||||||
|
|
||||||
public TableConfig(DbContextConfig config, Type tableType, string propertyName, int nthTable) {
|
public TableConfig(ITableGroupConfig config, Type tableType, string propertyName, int nthTable) {
|
||||||
TableType = tableType;
|
TableType = tableType;
|
||||||
PropertyName = propertyName;
|
PropertyName = propertyName;
|
||||||
ContextConfig = config;
|
ContextConfig = config;
|
||||||
|
|||||||
24
src/HopFrame.Core/Repositories/IHopFrameRepository.cs
Normal file
24
src/HopFrame.Core/Repositories/IHopFrameRepository.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace HopFrame.Core.Repositories;
|
||||||
|
|
||||||
|
public interface IHopFrameRepository<TModel, in TKey> where TModel : class {
|
||||||
|
|
||||||
|
Task<IEnumerable<TModel>> LoadPage(int page, int perPage);
|
||||||
|
|
||||||
|
Task<SearchResult<TModel>> Search(string searchTerm, int page, int perPage);
|
||||||
|
|
||||||
|
Task<int> GetTotalPageCount(int perPage);
|
||||||
|
|
||||||
|
Task CreateItem(TModel item);
|
||||||
|
|
||||||
|
Task EditItem(TModel item);
|
||||||
|
|
||||||
|
Task DeleteItem(TModel item);
|
||||||
|
|
||||||
|
Task<TModel?> GetOne(TKey key);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct SearchResult<TModel>(IEnumerable<TModel> items, int pageCount) {
|
||||||
|
public IEnumerable<TModel> Items { get; init; } = items;
|
||||||
|
public int PageCount { get; init; } = pageCount;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using HopFrame.Core.Config;
|
|||||||
namespace HopFrame.Core.Services;
|
namespace HopFrame.Core.Services;
|
||||||
|
|
||||||
public interface ITableManager {
|
public interface ITableManager {
|
||||||
public IQueryable<object> LoadPage(int page, int perPage = 20);
|
public Task<IEnumerable<object>> LoadPage(int page, int perPage = 20);
|
||||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20);
|
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20);
|
||||||
public Task<int> TotalPages(int perPage = 20);
|
public Task<int> TotalPages(int perPage = 20);
|
||||||
public Task DeleteItem(object item);
|
public Task DeleteItem(object item);
|
||||||
|
|||||||
@@ -45,11 +45,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tablePropertyName);
|
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tablePropertyName);
|
||||||
if (table is null) continue;
|
if (table is null) continue;
|
||||||
|
|
||||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
var repo = provider.GetService(context.ContextType);
|
||||||
if (dbContext is null) return null;
|
if (repo is null) return null;
|
||||||
|
|
||||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
if (context is DbContextConfig) {
|
||||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||||
|
return Activator.CreateInstance(type, (DbContext)repo, table, this, provider) as ITableManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context is RepositoryGroupConfig repoConfig) {
|
||||||
|
var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType);
|
||||||
|
return Activator.CreateInstance(type, repo, this, provider) as ITableManager;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -60,11 +67,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
var table = context.Tables.FirstOrDefault(table => table.TableType == tableType);
|
var table = context.Tables.FirstOrDefault(table => table.TableType == tableType);
|
||||||
if (table is null) continue;
|
if (table is null) continue;
|
||||||
|
|
||||||
var dbContext = provider.GetService(context.ContextType) as DbContext;
|
var repo = provider.GetService(context.ContextType);
|
||||||
if (dbContext is null) return null;
|
if (repo is null) return null;
|
||||||
|
|
||||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
if (context is DbContextConfig) {
|
||||||
return Activator.CreateInstance(type, dbContext, table, this, provider) as ITableManager;
|
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||||
|
return Activator.CreateInstance(type, (DbContext)repo, table, this, provider) as ITableManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context is RepositoryGroupConfig repoConfig) {
|
||||||
|
var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType);
|
||||||
|
return Activator.CreateInstance(type, repo, this, provider) as ITableManager;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -72,6 +86,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
|
|
||||||
private void SeedTableData(TableConfig table) {
|
private void SeedTableData(TableConfig table) {
|
||||||
if (table.Seeded) return;
|
if (table.Seeded) return;
|
||||||
|
if (table.ContextConfig is not DbContextConfig) return;
|
||||||
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
var dbContext = (provider.GetRequiredService(table.ContextConfig.ContextType) as DbContext)!;
|
||||||
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using HopFrame.Core.Config;
|
||||||
|
using HopFrame.Core.Repositories;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.Services.Implementations;
|
||||||
|
|
||||||
|
public class RepositoryTableManager<TModel, TKey>(IHopFrameRepository<TModel, TKey> repo, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class {
|
||||||
|
public async Task<IEnumerable<object>> LoadPage(int page, int perPage = 20) {
|
||||||
|
return await repo.LoadPage(page, perPage);
|
||||||
|
}
|
||||||
|
public async Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||||
|
var result = await repo.Search(searchTerm, page, perPage);
|
||||||
|
return (result.Items, result.PageCount);
|
||||||
|
}
|
||||||
|
public Task<int> TotalPages(int perPage = 20) {
|
||||||
|
return repo.GetTotalPageCount(perPage);
|
||||||
|
}
|
||||||
|
public Task DeleteItem(object item) {
|
||||||
|
return repo.DeleteItem((TModel)item);
|
||||||
|
}
|
||||||
|
public Task EditItem(object item) {
|
||||||
|
return repo.EditItem((TModel)item);
|
||||||
|
}
|
||||||
|
public Task AddItem(object item) {
|
||||||
|
return repo.CreateItem((TModel)item);
|
||||||
|
}
|
||||||
|
public Task AddAll(IEnumerable<object> items) {
|
||||||
|
var tasks = items
|
||||||
|
.Select(item => repo.CreateItem((TModel)item))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
public async Task<object?> GetOne(object key) {
|
||||||
|
return await repo.GetOne((TKey)key);
|
||||||
|
}
|
||||||
|
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
|
||||||
|
var manager = new TableManager<TModel>(null!, null!, explorer, provider);
|
||||||
|
return await manager.DisplayProperty(item, prop, value, enumerableValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,13 @@ namespace HopFrame.Core.Services.Implementations;
|
|||||||
|
|
||||||
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class {
|
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class {
|
||||||
|
|
||||||
public IQueryable<object> LoadPage(int page, int perPage = 20) {
|
public async Task<IEnumerable<object>> LoadPage(int page, int perPage = 20) {
|
||||||
var table = context.Set<TModel>();
|
var table = context.Set<TModel>();
|
||||||
var data = IncludeForeignKeys(table);
|
var data = IncludeForeignKeys(table);
|
||||||
return data
|
return await data
|
||||||
.Skip(page * perPage)
|
.Skip(page * perPage)
|
||||||
.Take(perPage);
|
.Take(perPage)
|
||||||
|
.ToArrayAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
public Task<(IEnumerable<object>, int)> Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||||
|
|||||||
@@ -340,7 +340,7 @@
|
|||||||
|
|
||||||
var relationType = config.Info.PropertyType;
|
var relationType = config.Info.PropertyType;
|
||||||
if (config.IsEnumerable) {
|
if (config.IsEnumerable) {
|
||||||
relationType = config.Info.PropertyType.GetGenericArguments().First();
|
relationType = relationType.GetGenericArguments().First();
|
||||||
}
|
}
|
||||||
|
|
||||||
var relationTable = Explorer.GetTable(relationType);
|
var relationTable = Explorer.GetTable(relationType);
|
||||||
|
|||||||
@@ -231,7 +231,7 @@
|
|||||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||||
|
|
||||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||||
CurrentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
CurrentlyDisplayedModels = (await _manager!.LoadPage(_currentPage, PerPage)).ToArray();
|
||||||
_totalPages = await _manager.TotalPages(PerPage);
|
_totalPages = await _manager.TotalPages(PerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ public static class HopFrameConfiguratorExtensions {
|
|||||||
return configurator;
|
return configurator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the Exporter Plugin for data import/export functionality.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configurator">The configurator for the HopFrame configuration.</param>
|
||||||
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
|
public static HopFrameConfigurator AddExporters(this HopFrameConfigurator configurator) {
|
||||||
configurator.AddPlugin<ExporterPlugin>();
|
configurator.AddPlugin<ExporterPlugin>();
|
||||||
return configurator;
|
return configurator;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
|
|
||||||
[EventHandler]
|
[EventHandler]
|
||||||
public void OnInit(TableInitializedEvent e) {
|
public void OnInit(TableInitializedEvent e) {
|
||||||
|
if (e.Sender.DialogData is not null) return;
|
||||||
|
|
||||||
e.AddPageButton("Export", () => Export(e.Table), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowUpload());
|
e.AddPageButton("Export", () => Export(e.Table), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowUpload());
|
||||||
e.AddPageButton("Import", () => Import(e.Table, e.Sender), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowDownload());
|
e.AddPageButton("Import", () => Import(e.Table, e.Sender), icon: new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size20.ArrowDownload());
|
||||||
}
|
}
|
||||||
@@ -29,8 +31,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
}
|
}
|
||||||
|
|
||||||
var data = await manager
|
var data = await manager
|
||||||
.LoadPage(0, int.MaxValue)
|
.LoadPage(0, int.MaxValue);
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
var properties = table.Properties.Where(prop => !prop.IsVirtualProperty).ToArray();
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
if (property is null) continue;
|
if (property is null) continue;
|
||||||
|
|
||||||
object? value = rowValues[i];
|
object? value = rowValues[i];
|
||||||
|
if (string.IsNullOrWhiteSpace((string)value)) continue;
|
||||||
|
|
||||||
if (property.IsEnumerable) {
|
if (property.IsEnumerable) {
|
||||||
if (!property.Info.PropertyType.IsGenericType) continue;
|
if (!property.Info.PropertyType.IsGenericType) continue;
|
||||||
@@ -102,7 +104,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
|
|
||||||
var enumerable = Activator.CreateInstance(property.Info.PropertyType);
|
var enumerable = Activator.CreateInstance(property.Info.PropertyType);
|
||||||
foreach (var key in values) {
|
foreach (var key in values) {
|
||||||
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType));
|
var entry = await relationManager.GetOne(ParseString(key, primaryKeyType)!);
|
||||||
if (entry is null) continue;
|
if (entry is null) continue;
|
||||||
|
|
||||||
addMethod.Invoke(enumerable, [entry]);
|
addMethod.Invoke(enumerable, [entry]);
|
||||||
@@ -116,7 +118,7 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
var relationManager = explorer.GetTableManager(property.Info.PropertyType);
|
var relationManager = explorer.GetTableManager(property.Info.PropertyType);
|
||||||
var relationPrimaryKeyType = GetPrimaryKeyType(property.Info.PropertyType);
|
var relationPrimaryKeyType = GetPrimaryKeyType(property.Info.PropertyType);
|
||||||
if (relationManager is null || relationPrimaryKeyType is null) continue;
|
if (relationManager is null || relationPrimaryKeyType is null) continue;
|
||||||
value = await relationManager.GetOne(ParseString((string)value, relationPrimaryKeyType));
|
value = await relationManager.GetOne(ParseString((string)value, relationPrimaryKeyType)!);
|
||||||
}
|
}
|
||||||
else if (property.Info.PropertyType == typeof(Guid)) {
|
else if (property.Info.PropertyType == typeof(Guid)) {
|
||||||
var success = Guid.TryParse((string)value, out var guid);
|
var success = Guid.TryParse((string)value, out var guid);
|
||||||
@@ -151,13 +153,20 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
|
|
||||||
if (property.IsEnumerable) {
|
if (property.IsEnumerable) {
|
||||||
var enumerable = (IEnumerable)value;
|
var enumerable = (IEnumerable)value;
|
||||||
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o) ?? o.ToString())) + ']';
|
return '[' + string.Join(',', enumerable.OfType<object>().Select(o => SelectPrimaryKey(o, property) ?? o.ToString())) + ']';
|
||||||
}
|
}
|
||||||
|
|
||||||
return SelectPrimaryKey(value) ?? value.ToString() ?? string.Empty;
|
return SelectPrimaryKey(value, property) ?? value.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? SelectPrimaryKey(object entity) {
|
private string? SelectPrimaryKey(object entity, PropertyConfig config) {
|
||||||
|
if (config.IsRelation) {
|
||||||
|
var table = explorer.GetTable(entity.GetType());
|
||||||
|
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||||
|
return repoConfig.KeyProperty.GetValue(entity)?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return entity
|
return entity
|
||||||
.GetType()
|
.GetType()
|
||||||
.GetProperties()
|
.GetProperties()
|
||||||
@@ -169,6 +178,11 @@ internal sealed class ExporterPlugin(IContextExplorer explorer, IToastService to
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Type? GetPrimaryKeyType(Type tableType) {
|
private Type? GetPrimaryKeyType(Type tableType) {
|
||||||
|
var table = explorer.GetTable(tableType);
|
||||||
|
if (table?.ContextConfig is RepositoryGroupConfig repoConfig) {
|
||||||
|
return repoConfig.KeyProperty.PropertyType;
|
||||||
|
}
|
||||||
|
|
||||||
return tableType
|
return tableType
|
||||||
.GetProperties()
|
.GetProperties()
|
||||||
.FirstOrDefault(prop => prop
|
.FirstOrDefault(prop => prop
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
|
|
||||||
namespace HopFrame.Web.Services;
|
namespace HopFrame.Web.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides file handling capabilities for downloading and uploading files.
|
||||||
|
/// </summary>
|
||||||
public interface IFileService {
|
public interface IFileService {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initiates a file download with the specified name and data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the file to be downloaded.</param>
|
||||||
|
/// <param name="data">The byte array representing the file's content.</param>
|
||||||
public Task DownloadFile(string name, byte[] data);
|
public Task DownloadFile(string name, byte[] data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows the user to upload a file and returns the uploaded file for processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that returns an IBrowserFile representing the uploaded file.</returns>
|
||||||
public Task<IBrowserFile> UploadFile();
|
public Task<IBrowserFile> UploadFile();
|
||||||
|
|
||||||
}
|
}
|
||||||
59
testing/HopFrame.Testing/Models/Guest.cs
Normal file
59
testing/HopFrame.Testing/Models/Guest.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using HopFrame.Core.Repositories;
|
||||||
|
|
||||||
|
namespace HopFrame.Testing.Models;
|
||||||
|
|
||||||
|
public class Guest {
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public List<Message> Messages { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GuestRepository : IHopFrameRepository<Guest, int> {
|
||||||
|
|
||||||
|
public List<Guest> Guests { get; } = new();
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Guest>> LoadPage(int page, int perPage) {
|
||||||
|
return Guests
|
||||||
|
.Skip(page * perPage)
|
||||||
|
.Take(perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SearchResult<Guest>> Search(string searchTerm, int page, int perPage) {
|
||||||
|
var results = Guests
|
||||||
|
.Where(message => message.Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling(results.Length / (double)perPage);
|
||||||
|
return new SearchResult<Guest>(results
|
||||||
|
.Skip(page * perPage)
|
||||||
|
.Take(perPage), totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalPageCount(int perPage) {
|
||||||
|
return (int)Math.Ceiling(Guests.Count / (double)perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateItem(Guest item) {
|
||||||
|
Guests.Add(item);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task EditItem(Guest item) {
|
||||||
|
var old = Guests.Find(m => m.Id == item.Id);
|
||||||
|
if (old is not null)
|
||||||
|
Guests.Remove(old);
|
||||||
|
|
||||||
|
Guests.Add(item);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteItem(Guest item) {
|
||||||
|
Guests.Remove(item);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guest?> GetOne(int key) {
|
||||||
|
return Guests.Find(m => m.Id == key);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
testing/HopFrame.Testing/Models/Message.cs
Normal file
59
testing/HopFrame.Testing/Models/Message.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using HopFrame.Core.Repositories;
|
||||||
|
|
||||||
|
namespace HopFrame.Testing.Models;
|
||||||
|
|
||||||
|
public class Message {
|
||||||
|
public required int MessageIdentifier { get; set; }
|
||||||
|
public required User Sender { get; set; }
|
||||||
|
public required Guest Receiver { get; set; }
|
||||||
|
public required string Content { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessageRepository : IHopFrameRepository<Message, int> {
|
||||||
|
|
||||||
|
public List<Message> Messages { get; } = new();
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Message>> LoadPage(int page, int perPage) {
|
||||||
|
return Messages
|
||||||
|
.Skip(page * perPage)
|
||||||
|
.Take(perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SearchResult<Message>> Search(string searchTerm, int page, int perPage) {
|
||||||
|
var results = Messages
|
||||||
|
.Where(message => message.Content.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling(results.Length / (double)perPage);
|
||||||
|
return new SearchResult<Message>(results
|
||||||
|
.Skip(page * perPage)
|
||||||
|
.Take(perPage), totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalPageCount(int perPage) {
|
||||||
|
return (int)Math.Ceiling(Messages.Count / (double)perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateItem(Message item) {
|
||||||
|
Messages.Add(item);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task EditItem(Message item) {
|
||||||
|
var old = Messages.Find(m => m.MessageIdentifier == item.MessageIdentifier);
|
||||||
|
if (old is not null)
|
||||||
|
Messages.Remove(old);
|
||||||
|
|
||||||
|
Messages.Add(item);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteItem(Message item) {
|
||||||
|
Messages.Remove(item);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Message?> GetOne(int key) {
|
||||||
|
return Messages.Find(m => m.MessageIdentifier == key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using HopFrame.Testing.Components;
|
|||||||
using HopFrame.Testing.Models;
|
using HopFrame.Testing.Models;
|
||||||
using HopFrame.Web;
|
using HopFrame.Web;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Message = HopFrame.Testing.Models.Message;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -88,8 +89,31 @@ builder.Services.AddHopFrame(options => {
|
|||||||
.SetPolicy("counter.view");
|
.SetPolicy("counter.view");
|
||||||
|
|
||||||
options.AddExporters();
|
options.AddExporters();
|
||||||
|
|
||||||
|
options.AddCustomRepository<GuestRepository, Guest, int>(g => g.Id, table => {
|
||||||
|
table.SetDisplayName("Guests");
|
||||||
|
|
||||||
|
table.Property(g => g.Messages)
|
||||||
|
.ForceRelation(true)
|
||||||
|
.FormatEach<Message>((m, _) => m.Content);
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddCustomRepository<MessageRepository, Message, int>(m => m.MessageIdentifier, table => {
|
||||||
|
table.SetDisplayName("Messages");
|
||||||
|
|
||||||
|
table.Property(m => m.Receiver)
|
||||||
|
.ForceRelation()
|
||||||
|
.Format((u, _) => u.Name);
|
||||||
|
|
||||||
|
table.Property(m => m.Sender)
|
||||||
|
.ForceRelation()
|
||||||
|
.Format((u, _) => u.Username ?? string.Empty);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<MessageRepository>();
|
||||||
|
builder.Services.AddSingleton<GuestRepository>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class TableManagerTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LoadPage_ReturnsPagedData() {
|
public async Task LoadPage_ReturnsPagedData() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var data = new List<MockModel> {
|
var data = new List<MockModel> {
|
||||||
new MockModel { Id = 1, Name = "Item1" },
|
new MockModel { Id = 1, Name = "Item1" },
|
||||||
@@ -54,7 +54,7 @@ public class TableManagerTests {
|
|||||||
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
var manager = new TableManager<MockModel>(dbContext.Object, config, explorer.Object, provider.Object);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = manager.LoadPage(1, 2).ToList();
|
var result = (await manager.LoadPage(1, 2)).ToArray();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Single(result);
|
Assert.Single(result);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class HopFrameTablePageTests : TestContext {
|
|||||||
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
contextExplorerMock.Setup(e => e.GetTable("Table1")).Returns(tableConfig);
|
||||||
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(managerMock.Object);
|
contextExplorerMock.Setup(e => e.GetTableManager("Table1")).Returns(managerMock.Object);
|
||||||
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
|
authHandlerMock.Setup(h => h.IsAuthenticatedAsync(It.IsAny<string>())).ReturnsAsync(true);
|
||||||
managerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(Enumerable.Empty<object>().AsAsyncQueryable());
|
managerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync([]);
|
||||||
|
|
||||||
Services.AddHopFrame(config, null, false);
|
Services.AddHopFrame(config, null, false);
|
||||||
Services.AddSingleton(contextExplorerMock.Object);
|
Services.AddSingleton(contextExplorerMock.Object);
|
||||||
@@ -71,7 +71,7 @@ public class HopFrameTablePageTests : TestContext {
|
|||||||
|
|
||||||
var tableManagerMock = new Mock<ITableManager>();
|
var tableManagerMock = new Mock<ITableManager>();
|
||||||
var items = new List<object> { new MyTable(), new MyTable() };
|
var items = new List<object> { new MyTable(), new MyTable() };
|
||||||
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).Returns(items.AsAsyncQueryable());
|
tableManagerMock.Setup(m => m.LoadPage(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync(items);
|
||||||
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
|
tableManagerMock.Setup(t => t.DisplayProperty(It.IsAny<object>(), It.IsAny<PropertyConfig>(), null, null))
|
||||||
.ReturnsAsync(string.Empty);
|
.ReturnsAsync(string.Empty);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user