diff --git a/src/System Application/App/Entitlements/DelegatedAdminagentPartner.Entitlement.al b/src/System Application/App/Entitlements/DelegatedAdminagentPartner.Entitlement.al
index 821dd6739d..cd2466277f 100644
--- a/src/System Application/App/Entitlements/DelegatedAdminagentPartner.Entitlement.al
+++ b/src/System Application/App/Entitlements/DelegatedAdminagentPartner.Entitlement.al
@@ -8,6 +8,7 @@ namespace System.Security.AccessControl;
using System.Azure.Identity;
using System.Environment.Configuration;
using System.Email;
+using System.ExternalFileStorage;
using System.Apps;
using System.Integration;
@@ -25,6 +26,7 @@ entitlement "Delegated Admin agent - Partner"
"Exten. Mgt. - Admin",
"Email - Admin",
"Feature Key - Admin",
+ "File Storage - Admin",
"VSC Intgr. - Admin";
#pragma warning restore
}
diff --git a/src/System Application/App/External File Storage/README.md b/src/System Application/App/External File Storage/README.md
new file mode 100644
index 0000000000..9822bca4b9
--- /dev/null
+++ b/src/System Application/App/External File Storage/README.md
@@ -0,0 +1,7 @@
+# External File Storage Module for Business Central
+Provides an API that lets you connect external cloud storage accounts to Business Central, allowing users to access files stored outside of Business Central.
+
+## Main Components
+
+### File Account
+A file account holds the information needed to access an external storage service from Business Central.
diff --git a/src/System Application/App/External File Storage/app.json b/src/System Application/App/External File Storage/app.json
new file mode 100644
index 0000000000..afc4ce7240
--- /dev/null
+++ b/src/System Application/App/External File Storage/app.json
@@ -0,0 +1,70 @@
+{
+ "id": "c9c54414-80c3-4cc9-98c6-589158882774",
+ "name": "External File Storage",
+ "publisher": "Microsoft",
+ "brief": "Access external file storage services",
+ "description": "Enables access to external file storage services from Business Central.",
+ "version": "26.0.0.0",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
+ "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
+ "help": "https://go.microsoft.com/fwlink/?linkid=2103698",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "",
+ "dependencies": [
+ {
+ "id": "7e3b999e-1182-45d2-8b82-d5127ddba9b2",
+ "name": "DotNet Aliases",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "872fe7e8-9893-40ae-ab94-c123ed30fdbd",
+ "name": "Extension Management",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "de35f591-7216-4e60-8be1-1911d71a7fc2",
+ "name": "Telemetry",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "0846d207-5dec-4c1b-afd8-6a25e1e14b9d",
+ "name": "Base64 Convert",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "a4584a53-9345-458a-af20-d1df2fab7bd8",
+ "name": "Confirm Management",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "e31ad830-3d46-472e-afeb-1d3d35247943",
+ "name": "BLOB Storage",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ }
+ ],
+ "internalsVisibleTo": [
+ {
+ "id": "f188754b-3ffb-443a-9507-f5fbdae3af2c",
+ "name": "External File Storage Test Library",
+ "publisher": "Microsoft"
+ }
+ ],
+ "screenshots": [
+
+ ],
+ "platform": "26.0.0.0",
+ "idRanges": [
+ {
+ "from": 9450,
+ "to": 9459
+ }
+ ],
+ "target": "OnPrem",
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520"
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/permissions/FileStorageAdmin.PermissionSet.al b/src/System Application/App/External File Storage/permissions/FileStorageAdmin.PermissionSet.al
new file mode 100644
index 0000000000..8b7767fc97
--- /dev/null
+++ b/src/System Application/App/External File Storage/permissions/FileStorageAdmin.PermissionSet.al
@@ -0,0 +1,22 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+permissionset 9450 "File Storage - Admin"
+{
+ Access = Public;
+ Assignable = true;
+ Caption = 'External File Storage - Admin';
+
+ IncludedPermissionSets = "File Storage - Edit";
+
+ Permissions =
+ tabledata "Ext. File Storage Connector" = RIMD,
+ tabledata "File Storage Connector Logo" = RIMD,
+ tabledata "File Account Scenario" = RIMD,
+ tabledata "File Scenario" = RIMD,
+ tabledata "File Account Content" = RIMD;
+}
diff --git a/src/System Application/App/External File Storage/permissions/FileStorageEdit.PermissionSet.al b/src/System Application/App/External File Storage/permissions/FileStorageEdit.PermissionSet.al
new file mode 100644
index 0000000000..8ee91cd735
--- /dev/null
+++ b/src/System Application/App/External File Storage/permissions/FileStorageEdit.PermissionSet.al
@@ -0,0 +1,20 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+using System.Environment;
+
+permissionset 9453 "File Storage - Edit"
+{
+ Access = Public;
+ Assignable = false;
+ Caption = 'File Storage - Edit';
+
+ IncludedPermissionSets = "File Storage - Read";
+
+ Permissions = tabledata "File Storage Connector Logo" = imd,
+ tabledata "Tenant Media" = imd;
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/permissions/FileStorageObjects.PermissionSet.al b/src/System Application/App/External File Storage/permissions/FileStorageObjects.PermissionSet.al
new file mode 100644
index 0000000000..26a61b8e4b
--- /dev/null
+++ b/src/System Application/App/External File Storage/permissions/FileStorageObjects.PermissionSet.al
@@ -0,0 +1,17 @@
+namespace System.ExternalFileStorage;
+
+permissionset 9452 "File Storage - Objects"
+{
+ Access = Internal;
+ Assignable = false;
+
+ Permissions =
+ codeunit "File Account" = X,
+ codeunit "External File Storage" = X,
+ codeunit "File Pagination Data" = X,
+ codeunit "File Scenario" = X,
+ table "File Account" = X,
+ table "File Account Content" = X,
+ table "File Account Scenario" = X,
+ table "File Scenario" = X;
+}
diff --git a/src/System Application/App/External File Storage/permissions/FileStorageRead.PermissionSet.al b/src/System Application/App/External File Storage/permissions/FileStorageRead.PermissionSet.al
new file mode 100644
index 0000000000..77cd5661ec
--- /dev/null
+++ b/src/System Application/App/External File Storage/permissions/FileStorageRead.PermissionSet.al
@@ -0,0 +1,23 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+using System.Environment;
+
+permissionset 9451 "File Storage - Read"
+{
+ Access = Internal;
+ Assignable = false;
+ IncludedPermissionSets = "File Storage - Objects";
+
+ Permissions =
+ tabledata "Ext. File Storage Connector" = r,
+ tabledata "File Storage Connector Logo" = r,
+ tabledata "File Account Scenario" = r,
+ tabledata "File Scenario" = r,
+ tabledata "File Account Content" = r,
+ tabledata Media = r; // This permission is required by External File Storage Account Wizard
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Account/FileAccount.Codeunit.al b/src/System Application/App/External File Storage/src/Account/FileAccount.Codeunit.al
new file mode 100644
index 0000000000..5a2f974ae8
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Account/FileAccount.Codeunit.al
@@ -0,0 +1,46 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Provides functionality to work with file accounts.
+///
+
+codeunit 9450 "File Account"
+{
+ Access = Public;
+
+ ///
+ /// Gets all of the file accounts registered in Business Central.
+ ///
+ /// Flag, used to determine whether to load the logos for the accounts.
+ /// Out parameter holding the file accounts.
+ procedure GetAllAccounts(LoadLogos: Boolean; var TempFileAccount: Record "File Account" temporary)
+ begin
+ FileAccountImpl.GetAllAccounts(LoadLogos, TempFileAccount);
+ end;
+
+ ///
+ /// Gets all of the file accounts registered in Business Central.
+ ///
+ /// Out parameter holding the file accounts.
+ procedure GetAllAccounts(var TempFileAccount: Record "File Account" temporary)
+ begin
+ FileAccountImpl.GetAllAccounts(false, TempFileAccount);
+ end;
+
+ ///
+ /// Checks if there is at least one file account registered in Business Central.
+ ///
+ /// True if there is any account registered in the system, otherwise - false.
+ procedure IsAnyAccountRegistered(): Boolean
+ begin
+ exit(FileAccountImpl.IsAnyAccountRegistered());
+ end;
+
+ var
+ FileAccountImpl: Codeunit "File Account Impl.";
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Account/FileAccount.Table.al b/src/System Application/App/External File Storage/src/Account/FileAccount.Table.al
new file mode 100644
index 0000000000..b6919a95a8
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Account/FileAccount.Table.al
@@ -0,0 +1,43 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// A common representation of a file account.
+///
+table 9450 "File Account"
+{
+ Extensible = false;
+ TableType = Temporary;
+
+ fields
+ {
+ field(1; "Account Id"; Guid) { }
+ field(2; Name; Text[250]) { }
+ field(4; Connector; Enum "Ext. File Storage Connector") { }
+ field(5; Logo; Media)
+ {
+ Access = Internal;
+ }
+ }
+
+ keys
+ {
+ key(PK; "Account Id", Connector)
+ {
+ Clustered = true;
+ }
+ key(Name; Name)
+ {
+ Description = 'Used for sorting';
+ }
+ }
+
+ fieldgroups
+ {
+ fieldgroup(Brick; Logo, Name) { }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Account/FileAccountImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Account/FileAccountImpl.Codeunit.al
new file mode 100644
index 0000000000..24eeb83a75
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Account/FileAccountImpl.Codeunit.al
@@ -0,0 +1,227 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+using System.Text;
+using System.Utilities;
+
+codeunit 9451 "File Account Impl."
+{
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+ Permissions = tabledata "File Storage Connector Logo" = rimd,
+ tabledata "File Scenario" = imd;
+
+ procedure GetAllAccounts(LoadLogos: Boolean; var TempFileAccount: Record "File Account" temporary)
+ var
+ TempFileFileAccounts: Record "File Account" temporary;
+ FileSystemConnector: Interface "External File Storage Connector";
+ Connector: Enum "Ext. File Storage Connector";
+ begin
+ TempFileAccount.Reset();
+ TempFileAccount.DeleteAll();
+
+ foreach Connector in Connector.Ordinals do begin
+ FileSystemConnector := Connector;
+
+ TempFileFileAccounts.DeleteAll();
+ FileSystemConnector.GetAccounts(TempFileFileAccounts);
+
+ if TempFileFileAccounts.FindSet() then
+ repeat
+ TempFileAccount := TempFileFileAccounts;
+ TempFileAccount.Connector := Connector;
+
+ if LoadLogos then
+ ImportLogo(TempFileAccount, Connector);
+
+ TempFileAccount.Insert();
+ until TempFileFileAccounts.Next() = 0;
+ end;
+
+ // Sort by account name
+ TempFileAccount.SetCurrentKey(Name);
+ end;
+
+ procedure DeleteAccounts(var TempFileAccountsToDelete: Record "File Account" temporary)
+ var
+ TempCurrentDefaultFileAccount: Record "File Account" temporary;
+ ConfirmManagement: Codeunit "Confirm Management";
+ FileScenario: Codeunit "File Scenario";
+ FileSystemConnector: Interface "External File Storage Connector";
+ begin
+ CheckPermissions();
+
+ if not ConfirmManagement.GetResponseOrDefault(ConfirmDeleteQst, true) then
+ exit;
+
+ if not TempFileAccountsToDelete.FindSet() then
+ exit;
+
+ // Get the current default account to track if it was deleted
+ FileScenario.GetDefaultFileAccount(TempCurrentDefaultFileAccount);
+
+ // Delete all selected accounts
+ repeat
+ // Check to validate that the connector is still installed
+ // The connector could have been uninstalled by another user/session
+ if IsValidConnector(TempFileAccountsToDelete.Connector) then begin
+ FileSystemConnector := TempFileAccountsToDelete.Connector;
+ FileSystemConnector.DeleteAccount(TempFileAccountsToDelete."Account Id");
+ end;
+ until TempFileAccountsToDelete.Next() = 0;
+
+ DefaultAccountDeletion(TempCurrentDefaultFileAccount."Account Id", TempCurrentDefaultFileAccount.Connector);
+ end;
+
+ local procedure DefaultAccountDeletion(CurrentDefaultAccountId: Guid; Connector: Enum "Ext. File Storage Connector")
+ var
+ TempAllFileAccounts: Record "File Account" temporary;
+ TempNewDefaultFileAccount: Record "File Account" temporary;
+ FileScenario: Codeunit "File Scenario";
+ begin
+ GetAllAccounts(false, TempAllFileAccounts);
+
+ if TempAllFileAccounts.IsEmpty() then
+ exit; //All of the accounts were deleted, nothing to do
+
+ if TempAllFileAccounts.Get(CurrentDefaultAccountId, Connector) then
+ exit; // The default account was not deleted or it never existed
+
+ // In case there's only one account, set it as default
+ if TempAllFileAccounts.Count() = 1 then begin
+ MakeDefault(TempAllFileAccounts);
+ exit;
+ end;
+
+ Commit(); // Commit the accounts deletion in order to prompt for a new default account
+ if PromptNewDefaultAccountChoice(TempNewDefaultFileAccount) then
+ MakeDefault(TempNewDefaultFileAccount)
+ else
+ FileScenario.UnassignScenario(Enum::"File Scenario"::Default); // remove the default scenario as it is pointing to a non-existent account
+ end;
+
+ local procedure PromptNewDefaultAccountChoice(var TempNewDefaultFileAccount: Record "File Account" temporary): Boolean
+ var
+ FileAccountsPage: Page "File Accounts";
+ begin
+ FileAccountsPage.LookupMode(true);
+ FileAccountsPage.EnableLookupMode();
+ FileAccountsPage.Caption(ChooseNewDefaultTxt);
+ if FileAccountsPage.RunModal() <> Action::LookupOK then
+ exit;
+
+ FileAccountsPage.GetAccount(TempNewDefaultFileAccount);
+ exit(true);
+ end;
+
+ local procedure ImportLogo(var TempFileAccount: Record "File Account" temporary; FileSystemConnector: Interface "External File Storage Connector")
+ var
+ FileSystemConnectorLogo: Record "File Storage Connector Logo";
+ Base64Convert: Codeunit "Base64 Convert";
+ TempBlob: Codeunit "Temp Blob";
+ InStream: InStream;
+ ConnectorLogoDescriptionTxt: Label '%1 Logo', Locked = true;
+ OutStream: OutStream;
+ ConnectorLogoBase64: Text;
+ begin
+ ConnectorLogoBase64 := FileSystemConnector.GetLogoAsBase64();
+
+ if ConnectorLogoBase64 = '' then
+ exit;
+
+ if not FileSystemConnectorLogo.Get(TempFileAccount.Connector) then begin
+ TempBlob.CreateOutStream(OutStream);
+ Base64Convert.FromBase64(ConnectorLogoBase64, OutStream);
+ TempBlob.CreateInStream(InStream);
+ FileSystemConnectorLogo.Init();
+ FileSystemConnectorLogo.Connector := TempFileAccount.Connector;
+ FileSystemConnectorLogo.Logo.ImportStream(InStream, StrSubstNo(ConnectorLogoDescriptionTxt, TempFileAccount.Connector));
+ if FileSystemConnectorLogo.Insert() then;
+ end;
+ TempFileAccount.Logo := FileSystemConnectorLogo.Logo;
+ end;
+
+ procedure IsAnyAccountRegistered(): Boolean
+ var
+ TempFileAccount: Record "File Account" temporary;
+ begin
+ GetAllAccounts(false, TempFileAccount);
+
+ exit(not TempFileAccount.IsEmpty());
+ end;
+
+ procedure IsUserFileAdmin(): Boolean
+ var
+ FileScenario: Record "File Scenario";
+ begin
+ exit(FileScenario.WritePermission());
+ end;
+
+ procedure FindAllConnectors(var TempFileConnector: Record "Ext. File Storage Connector" temporary)
+ var
+ FileConnectorLogo: Record "File Storage Connector Logo";
+ ConnectorInterface: Interface "External File Storage Connector";
+ FileSystemConnector: Enum "Ext. File Storage Connector";
+ begin
+ foreach FileSystemConnector in Enum::"Ext. File Storage Connector".Ordinals() do begin
+ ConnectorInterface := FileSystemConnector;
+ TempFileConnector.Connector := FileSystemConnector;
+ TempFileConnector.Description := ConnectorInterface.GetDescription();
+ if FileConnectorLogo.Get(TempFileConnector.Connector) then
+ TempFileConnector.Logo := FileConnectorLogo.Logo;
+ TempFileConnector.Insert();
+ end;
+ end;
+
+ procedure IsValidConnector(Connector: Enum "Ext. File Storage Connector"): Boolean
+ begin
+ exit("Ext. File Storage Connector".Ordinals().Contains(Connector.AsInteger()));
+ end;
+
+ procedure MakeDefault(var TempFileAccount: Record "File Account" temporary)
+ var
+ FileScenario: Codeunit "File Scenario";
+ begin
+ CheckPermissions();
+
+ if IsNullGuid(TempFileAccount."Account Id") then
+ exit;
+
+ FileScenario.SetDefaultFileAccount(TempFileAccount);
+ end;
+
+ procedure BrowseAccount(var TempFileAccount: Record "File Account" temporary)
+ var
+ StorageBrowser: Page "Storage Browser";
+ begin
+ CheckPermissions();
+
+ if IsNullGuid(TempFileAccount."Account Id") then
+ exit;
+
+ StorageBrowser.SetFileAccount(TempFileAccount);
+ StorageBrowser.BrowseFileAccount('');
+ StorageBrowser.Run();
+ end;
+
+ procedure CheckPermissions()
+ begin
+ if not IsUserFileAdmin() then
+ Error(CannotManageSetupErr);
+ end;
+
+ [InternalEvent(false)]
+ procedure OnAfterSetSelectionFilter(var TempFileAccount: Record "File Account" temporary)
+ begin
+ end;
+
+ var
+ CannotManageSetupErr: Label 'Your user account does not give you permission to set up file accounts. Please contact your administrator.';
+ ChooseNewDefaultTxt: Label 'Choose a Default Account';
+ ConfirmDeleteQst: Label 'Go ahead and delete?';
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Account/FileAccountWizard.Page.al b/src/System Application/App/External File Storage/src/Account/FileAccountWizard.Page.al
new file mode 100644
index 0000000000..8bf0dded1a
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Account/FileAccountWizard.Page.al
@@ -0,0 +1,490 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+using System.Apps;
+using System.Environment;
+using System.Telemetry;
+
+///
+/// Step by step guide for adding a new file account in Business Central
+///
+page 9451 "File Account Wizard"
+{
+ PageType = NavigatePage;
+ ApplicationArea = All;
+ UsageCategory = Administration;
+ Caption = 'Set Up File Accounts';
+ SourceTable = "Ext. File Storage Connector";
+ SourceTableTemporary = true;
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+ Editable = true;
+ ShowFilter = false;
+ LinksAllowed = false;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+ Permissions = tabledata Media = r,
+ tabledata "Media Resources" = r;
+
+ layout
+ {
+ area(Content)
+ {
+ group(Done)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Visible = not DoneVisible and TopBannerVisible;
+ field(NotDoneIcon; MediaResourcesStandard."Media Reference")
+ {
+ Editable = false;
+ ShowCaption = false;
+ ToolTip = ' ', Locked = true;
+ }
+ }
+ group(NotDone)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Visible = DoneVisible and TopBannerVisible;
+ field(DoneIcon; MediaResourcesDone."Media Reference")
+ {
+ Editable = false;
+ ShowCaption = false;
+ ToolTip = ' ', Locked = true;
+ }
+ }
+
+ group(Header)
+ {
+ ShowCaption = false;
+ Visible = WelcomeVisible;
+
+ group(HeaderText)
+ {
+ Caption = 'Welcome to file in Business Central';
+ InstructionalText = 'Make file communications easier by connecting file accounts to Business Central. For example, store sales quotes and orders pdfs without opening an file app.';
+ }
+ field(LearnMoreHeader; LearnMoreTok)
+ {
+ Editable = false;
+ ShowCaption = false;
+ ToolTip = 'View information about how to set up the file capabilities.';
+
+ trigger OnDrillDown()
+ begin
+ Hyperlink(LearnMoreURLTxt);
+ end;
+ }
+ group(Privacy)
+ {
+ Caption = 'Privacy notice';
+ InstructionalText = 'By adding a file account you acknowledge that the file provider might be able to access the data you send in files from Business Central.';
+ }
+ group(GetStartedText)
+ {
+ Caption = 'Let''s go!';
+ InstructionalText = 'Choose Next to get started.';
+ }
+ }
+
+ group(ConnectorHeader)
+ {
+ ShowCaption = false;
+ Visible = ChooseConnectorVisible and ConnectorsAvailable;
+
+ label(UsageWarning)
+ {
+ Caption = 'Use caution when adding file accounts. Depending on your setup, accounts can be available to all users.';
+ }
+ }
+
+ group(ConnectorsGroup)
+ {
+ Visible = ChooseConnectorVisible and ConnectorsAvailable;
+ label("Specify the type of file account to add")
+ {
+ Caption = 'Specify the type of file account to add';
+ }
+ repeater(Connectors)
+ {
+ ShowCaption = false;
+ Visible = ChooseConnectorVisible and ConnectorsAvailable;
+ FreezeColumn = Name;
+ Editable = false;
+
+ field(Logo; Rec.Logo)
+ {
+ Caption = ' ';
+ Editable = false;
+ Visible = ChooseConnectorVisible;
+ ToolTip = 'Specifies the type of the account you want to create.';
+ ShowCaption = false;
+ Width = 1;
+ }
+ field(Name; Rec.Connector)
+ {
+ Caption = 'Account Type';
+ ToolTip = 'Specifies the type of the account you want to create.';
+ Editable = false;
+ }
+ field(Details; Rec.Description)
+ {
+ Caption = 'Details';
+ ToolTip = 'Specifies more details about the account type.';
+ Editable = false;
+ Width = 50;
+ }
+ }
+ }
+
+ group(NoConnectorsAvailableGroup)
+ {
+ Visible = ChooseConnectorVisible and not ConnectorsAvailable;
+ label(NoConnectorsAvailable)
+ {
+ Caption = 'There are no file apps available. To use this feature you must install an file app.';
+ }
+ label(NoConnectorsAvailable2)
+ {
+ Caption = 'File apps are available in Extension Management and AppSource.';
+ }
+ field(ExtensionManagement; ExtensionManagementTok)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Caption = ' ';
+ ToolTip = 'Navigate to Extension Management page.';
+
+ trigger OnDrillDown()
+ begin
+ Page.Run(Page::"Extension Management");
+ end;
+ }
+ field(AppSource; AppSourceTok)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Visible = AppSourceAvailable;
+ Caption = ' ';
+ ToolTip = 'Navigate to AppSource.';
+
+ trigger OnDrillDown()
+ begin
+ AppSource := AppSource.Create();
+ AppSource.ShowAppSource();
+ end;
+ }
+ label(NoConnectorsAvailable3)
+ {
+ Caption = 'View a list of the available file apps';
+ }
+ field(LearnMore; LearnMoreTok)
+ {
+ Editable = false;
+ ShowCaption = false;
+ Caption = ' ';
+ ToolTip = 'View information about how to set up the file capabilities.';
+
+ trigger OnDrillDown()
+ begin
+ Hyperlink(LearnMoreURLTxt);
+ end;
+ }
+ }
+
+ group(LastPage)
+ {
+ Visible = DoneVisible;
+
+ group(AllSet)
+ {
+ Caption = 'Congratulations!';
+ InstructionalText = 'You have successfully added the file account. To check that it is working, send a test file.';
+ }
+ group(Account)
+ {
+ Caption = 'Account';
+ field(NameField; TempRegisteredAccount.Name)
+ {
+ Editable = false;
+ Caption = 'Name';
+ ToolTip = 'Specifies the name of the account registered.';
+ }
+ }
+ group(Default)
+ {
+ Caption = '';
+
+ field(DefaultField; SetAsDefault)
+ {
+ Editable = true;
+ Enabled = true;
+ Caption = 'Set as default';
+ ToolTip = 'Specifies the the account for all scenarios.';
+ }
+ }
+ }
+ }
+ }
+
+ actions
+ {
+ area(Processing)
+ {
+ action(Cancel)
+ {
+ Visible = CancelActionVisible;
+ Caption = 'Cancel';
+ ToolTip = 'Cancel';
+ InFooterBar = true;
+ Image = Cancel;
+
+ trigger OnAction()
+ begin
+ CurrPage.Close();
+ end;
+ }
+
+ action(Back)
+ {
+ Visible = BackActionVisible;
+ Enabled = BackActionEnabled;
+ Caption = 'Back';
+ ToolTip = 'Back';
+ InFooterBar = true;
+ Image = PreviousRecord;
+
+ trigger OnAction()
+ begin
+ NextStep(true);
+ end;
+ }
+
+ action(Next)
+ {
+ Visible = NextActionVisible;
+ Enabled = NextActionEnabled;
+ Caption = 'Next';
+ ToolTip = 'Next';
+ InFooterBar = true;
+ Image = NextRecord;
+
+ trigger OnAction()
+ begin
+ NextStep(false);
+ end;
+ }
+
+ action(Finish)
+ {
+ Visible = FinishActionVisible;
+ Caption = 'Finish';
+ ToolTip = 'Finish';
+ InFooterBar = true;
+ Image = NextRecord;
+
+ trigger OnAction()
+ var
+ FileAccountImpl: Codeunit "File Account Impl.";
+ begin
+ if SetAsDefault then
+ FileAccountImpl.MakeDefault(TempRegisteredAccount);
+
+ CurrPage.Close();
+ end;
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ begin
+ StartTime := CurrentDateTime();
+ end;
+
+ trigger OnQueryClosePage(CloseAction: Action): Boolean
+ var
+ DurationAsInt: Integer;
+ begin
+ DurationAsInt := CurrentDateTime() - StartTime;
+ if Step = Step::Done then
+ Session.LogMessage('0000CTK', StrSubstNo(AccountCreationSuccessfullyCompletedDurationLbl, DurationAsInt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', FileCategoryLbl)
+ else
+ Session.LogMessage('0000CTL', StrSubstNo(AccountCreationFailureDurationLbl, DurationAsInt), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', FileCategoryLbl);
+ end;
+
+ trigger OnInit()
+ var
+ TempDefaultAccount: Record "File Account" temporary;
+ FileAccountImpl: Codeunit "File Account Impl.";
+ FileScenario: Codeunit "File Scenario";
+ begin
+ FileAccountImpl.CheckPermissions();
+
+ Step := Step::Welcome;
+ SetDefaultControls();
+ ShowWelcomeStep();
+
+ FileAccountImpl.FindAllConnectors(Rec);
+
+ FileRateLimitDisplay := NoLimitTxt;
+
+ if not FileScenario.GetDefaultFileAccount(TempDefaultAccount) then
+ SetAsDefault := true;
+
+ ConnectorsAvailable := Rec.FindFirst(); // Set the focus on the first record
+ AppSourceAvailable := AppSource.IsAvailable();
+ LoadTopBanners();
+ end;
+
+ local procedure NextStep(Backwards: Boolean)
+ begin
+ if Backwards then
+ Step -= 1
+ else
+ Step += 1;
+
+ SetDefaultControls();
+
+ case Step of
+ Step::Welcome:
+ ShowWelcomeStep();
+ Step::"Choose Connector":
+ ShowChooseConnectorStep();
+ Step::"Register Account":
+ ShowRegisterAccountStep();
+ Step::Done:
+ ShowDoneStep();
+ end;
+ end;
+
+ local procedure ShowWelcomeStep()
+ begin
+ WelcomeVisible := true;
+ BackActionEnabled := false;
+ end;
+
+ local procedure ShowChooseConnectorStep()
+ begin
+ if not ConnectorsAvailable then
+ NextActionEnabled := false;
+
+ ChooseConnectorVisible := true;
+ end;
+
+ local procedure ShowRegisterAccountStep()
+ var
+ FeatureTelemetry: Codeunit "Feature Telemetry";
+ Telemetry: Codeunit Telemetry;
+ AccountWasRegistered: Boolean;
+ ConnectorSucceeded: Boolean;
+ CustomDimensions: Dictionary of [Text, Text];
+ TelemetryAccountRegisteredLbl: Label '%1 account has been setup.', Locked = true;
+ TelemetryAccountFailedtoRegisterLbl: Label '%1 account has failed to setup. Error: %2', Locked = true;
+ begin
+ ConnectorSucceeded := TryRegisterAccount(AccountWasRegistered);
+ CustomDimensions.Add('Category', FileCategoryLbl);
+
+ if AccountWasRegistered then begin
+ FeatureTelemetry.LogUptake('0000CTF', 'File Access', Enum::"Feature Uptake Status"::"Set up");
+ Telemetry.LogMessage('0000CTH', StrSubstNo(TelemetryAccountRegisteredLbl, Rec.Connector), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);
+ NextStep(false);
+ end else begin
+ Telemetry.LogMessage('0000CTI', StrSubstNo(TelemetryAccountFailedtoRegisterLbl, Rec.Connector, GetLastErrorCallStack()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);
+ NextStep(true);
+ end;
+
+ if not ConnectorSucceeded then
+ Error(GetLastErrorText());
+ end;
+
+ [TryFunction]
+ local procedure TryRegisterAccount(var AccountWasRegistered: Boolean)
+ var
+ FileAccountImpl: Codeunit "File Account Impl.";
+ FileConnector: Interface "External File Storage Connector";
+ begin
+ // Check to validate that the connector is still installed
+ // The connector could have been uninstalled by another user/session
+ if not FileAccountImpl.IsValidConnector(Rec.Connector) then
+ Error(FileConnectorHasBeenUninstalledMsg);
+
+ FileConnector := Rec.Connector;
+
+ ClearLastError();
+ AccountWasRegistered := FileConnector.RegisterAccount(TempRegisteredAccount);
+ TempRegisteredAccount.Connector := Rec.Connector;
+ end;
+
+ local procedure ShowDoneStep()
+ begin
+ DoneVisible := true;
+ BackActionVisible := false;
+ NextActionVisible := false;
+ CancelActionVisible := false;
+ FinishActionVisible := true;
+ end;
+
+ local procedure SetDefaultControls()
+ begin
+ // Actions
+ BackActionVisible := true;
+ BackActionEnabled := true;
+ NextActionVisible := true;
+ NextActionEnabled := true;
+ CancelActionVisible := true;
+ FinishActionVisible := false;
+
+ // Groups
+ WelcomeVisible := false;
+ ChooseConnectorVisible := false;
+ DoneVisible := false;
+ end;
+
+ local procedure LoadTopBanners()
+ var
+ AssistedSetupLogoTok: Label 'ASSISTEDSETUP-NOTEXT-400PX.PNG', Locked = true;
+ begin
+ if MediaResourcesStandard.Get(AssistedSetupLogoTok) and
+ MediaResourcesDone.Get(AssistedSetupLogoTok) and (CurrentClientType() = ClientType::Web)
+ then
+ TopBannerVisible := MediaResourcesDone."Media Reference".HasValue();
+ end;
+
+ var
+ TempRegisteredAccount: Record "File Account" temporary;
+ MediaResourcesStandard: Record "Media Resources";
+ MediaResourcesDone: Record "Media Resources";
+ [RunOnClient]
+ AppSource: DotNet AppSource;
+ Step: Option Welcome,"Choose Connector","Register Account",Done;
+ AppSourceTok: Label 'AppSource';
+ ExtensionManagementTok: Label 'Extension Management';
+ FileCategoryLbl: Label 'File', Locked = true;
+ LearnMoreURLTxt: Label 'https://go.microsoft.com/fwlink/?linkid=2134520', Locked = true; //TODO Replace with correct URL to new documentation
+ LearnMoreTok: Label 'Learn more';
+ NoLimitTxt: Label 'No limit';
+ AccountCreationSuccessfullyCompletedDurationLbl: Label 'Successful creation of account completed. Duration: %1 milliseconds.', Comment = '%1 - Duration', Locked = true;
+ AccountCreationFailureDurationLbl: Label 'Creation of account failed. Duration: %1 milliseconds.', Comment = '%1 - Duration', Locked = true;
+ FileConnectorHasBeenUninstalledMsg: Label 'The selected file extension has been uninstalled. You must reinstall the extension to add an account with it.';
+ AppSourceAvailable: Boolean;
+ TopBannerVisible: Boolean;
+ BackActionVisible: Boolean;
+ BackActionEnabled: Boolean;
+ NextActionVisible: Boolean;
+ NextActionEnabled: Boolean;
+ CancelActionVisible: Boolean;
+ FinishActionVisible: Boolean;
+ WelcomeVisible: Boolean;
+ ChooseConnectorVisible: Boolean;
+ DoneVisible: Boolean;
+ ConnectorsAvailable: Boolean;
+ SetAsDefault: Boolean;
+ StartTime: DateTime;
+ FileRateLimitDisplay: Text[250];
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Account/FileAccounts.Page.al b/src/System Application/App/External File Storage/src/Account/FileAccounts.Page.al
new file mode 100644
index 0000000000..17a3848759
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Account/FileAccounts.Page.al
@@ -0,0 +1,312 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+using System.Telemetry;
+
+///
+/// Lists all of the registered file accounts
+///
+page 9450 "File Accounts"
+{
+ PageType = List;
+ Caption = 'File Accounts';
+ ApplicationArea = All;
+ UsageCategory = Administration;
+ SourceTable = "File Account";
+ SourceTableTemporary = true;
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+ Editable = false;
+ ShowFilter = false;
+ LinksAllowed = false;
+ RefreshOnActivate = true;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(Accounts)
+ {
+ FreezeColumn = NameField;
+ field(LogoField; Rec.Logo)
+ {
+ ShowCaption = false;
+ Caption = ' ';
+ ToolTip = 'Specifies the logo for the type of file account.';
+ Width = 1;
+ }
+ field(NameField; Rec.Name)
+ {
+ ToolTip = 'Specifies the name of the account.';
+ Visible = not IsInLookupMode;
+
+ trigger OnDrillDown()
+ begin
+ ShowAccountInformation();
+ end;
+ }
+ field(NameFieldLookup; Rec.Name)
+ {
+ ToolTip = 'Specifies the name of the account.';
+ Visible = IsInLookupMode;
+ }
+ field(DefaultField; DefaultTxt)
+ {
+ Caption = 'Default';
+ ToolTip = 'Specifies whether the file account will be used for all scenarios for which an account is not specified. You must have a default file account, even if you have only one account.';
+ Visible = not IsInLookupMode;
+ }
+ field(FileConnector; Rec.Connector)
+ {
+ ToolTip = 'Specifies the type of file extension that the account is added to.';
+ Visible = false;
+ }
+ }
+ }
+
+ area(FactBoxes)
+ {
+ part(Scenarios; "File Scenarios FactBox")
+ {
+ Caption = 'File Scenarios';
+ SubPageLink = "Account Id" = field("Account Id"), Connector = field(Connector), Scenario = filter(<> Default);
+ }
+ }
+ }
+
+ actions
+ {
+ area(Creation)
+ {
+ action(View)
+ {
+ Image = View;
+ ToolTip = 'View settings for the file account.';
+ ShortcutKey = return;
+ Visible = false;
+
+ trigger OnAction()
+ begin
+ ShowAccountInformation();
+ end;
+ }
+
+ action(AddAccount)
+ {
+ Image = Add;
+ Caption = 'Add a file account';
+ ToolTip = 'Opens a File Account Wizard setup page in order to add a file account.';
+ Visible = (not IsInLookupMode) and CanUserManageFileSetup;
+
+ trigger OnAction()
+ begin
+ Page.RunModal(Page::"File Account Wizard");
+
+ UpdateFileAccounts();
+ end;
+ }
+ }
+ area(Processing)
+ {
+ action(MakeDefault)
+ {
+ Image = Default;
+ Caption = 'Set as default';
+ ToolTip = 'Mark the selected file account as the default account. This account will be used for all scenarios for which an account is not specified.';
+ Visible = (not IsInLookupMode) and CanUserManageFileSetup;
+ Scope = Repeater;
+ Enabled = not IsDefault;
+
+ trigger OnAction()
+ begin
+ FileAccountImpl.MakeDefault(Rec);
+
+ UpdateAccounts := true;
+ CurrPage.Update(false);
+ end;
+ }
+ action(StorageBrowser)
+ {
+ Image = BOMVersions;
+ Caption = 'Storage Browser';
+ ToolTip = 'Opens the Storage Browser and shows the content of the selected account.';
+ Visible = (not IsInLookupMode) and CanUserManageFileSetup;
+ Scope = Repeater;
+
+ trigger OnAction()
+ begin
+ FileAccountImpl.BrowseAccount(Rec);
+
+ UpdateAccounts := true;
+ CurrPage.Update(false);
+ end;
+ }
+
+ action(Delete)
+ {
+ Image = Delete;
+ Caption = 'Delete file account';
+ ToolTip = 'Delete the file account.';
+ Visible = (not IsInLookupMode) and CanUserManageFileSetup;
+ Scope = Repeater;
+
+ trigger OnAction()
+ begin
+ CurrPage.SetSelectionFilter(Rec);
+ FileAccountImpl.OnAfterSetSelectionFilter(Rec);
+
+ FileAccountImpl.DeleteAccounts(Rec);
+
+ UpdateFileAccounts();
+ end;
+ }
+ }
+ area(Navigation)
+ {
+ action(FileScenarioSetup)
+ {
+ Image = Answers;
+ Caption = 'File Scenarios';
+ ToolTip = 'Assign scenarios to the file accounts.';
+ Visible = not IsInLookupMode;
+
+ trigger OnAction()
+ var
+ FileScenarioSetup: Page "File Scenario Setup";
+ begin
+ FileScenarioSetup.SetFileAccountId(Rec."Account Id", Rec.Connector);
+ FileScenarioSetup.Run();
+ end;
+ }
+ }
+ area(Promoted)
+ {
+ group(Category_New)
+ {
+ Caption = 'New';
+
+ actionref(AddAccount_Promoted; AddAccount) { }
+ }
+ group(Category_Process)
+ {
+ Caption = 'Process';
+
+ actionref(MakeDefault_Promoted; MakeDefault) { }
+ actionref(StorageBrowser_Promoted; StorageBrowser) { }
+ actionref(Delete_Promoted; Delete) { }
+ }
+
+ group(Category_Category4)
+ {
+ Caption = 'Navigate';
+
+ actionref(FileScenarioSetup_Promoted; FileScenarioSetup) { }
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ var
+ FeatureTelemetry: Codeunit "Feature Telemetry";
+ begin
+ FeatureTelemetry.LogUptake('0000CTA', 'External File Storage', Enum::"Feature Uptake Status"::Discovered);
+ CanUserManageFileSetup := FileAccountImpl.IsUserFileAdmin();
+ Rec.SetCurrentKey("Account Id", Connector);
+ UpdateFileAccounts();
+ end;
+
+ trigger OnAfterGetRecord()
+ begin
+ // Updating the accounts is done via OnAfterGetRecord in the cases when an account was changed from the corresponding connector's page
+ if UpdateAccounts then begin
+ UpdateAccounts := false;
+ UpdateFileAccounts();
+ end;
+
+ DefaultTxt := '';
+
+ IsDefault := TempDefaultFileAccount."Account Id" = Rec."Account Id";
+ if IsDefault then
+ DefaultTxt := '✓';
+ end;
+
+ local procedure UpdateFileAccounts()
+ var
+ FileAccount: Codeunit "File Account";
+ FileScenario: Codeunit "File Scenario";
+ IsSelected: Boolean;
+ SelectedAccountId: Guid;
+ begin
+ // Maintain the same selected record after updating accounts.
+ SelectedAccountId := Rec."Account Id";
+ IsSelected := not IsNullGuid(SelectedAccountId);
+
+ FileAccount.GetAllAccounts(true, Rec); // Refresh the file accounts
+ FileScenario.GetDefaultFileAccount(TempDefaultFileAccount); // Refresh the default file account
+
+ if IsSelected then begin
+ Rec."Account Id" := SelectedAccountId;
+ if Rec.Find() then;
+ end else
+ if Rec.FindFirst() then;
+
+ CurrPage.Update(false);
+ end;
+
+ local procedure ShowAccountInformation()
+ var
+ Connector: Interface "External File Storage Connector";
+ begin
+ UpdateAccounts := true;
+
+ if not FileAccountImpl.IsValidConnector(Rec.Connector) then
+ Error(FileConnectorHasBeenUninstalledMsg);
+
+ Connector := Rec.Connector;
+ Connector.ShowAccountInformation(Rec."Account Id");
+ end;
+
+ ///
+ /// Gets the selected file account.
+ ///
+ /// The selected file account
+ procedure GetAccount(var TempFileAccount: Record "File Account" temporary)
+ begin
+ TempFileAccount := Rec;
+ end;
+
+ ///
+ /// Sets a file account to be selected.
+ ///
+ /// The file account to be initially selected on the page
+ procedure SetAccount(var TempFileAccount: Record "File Account" temporary)
+ begin
+ Rec := TempFileAccount;
+ end;
+
+ ///
+ /// Enables the lookup mode on the page.
+ ///
+ procedure EnableLookupMode()
+ begin
+ IsInLookupMode := true;
+ CurrPage.LookupMode(true);
+ end;
+
+ var
+ TempDefaultFileAccount: Record "File Account" temporary;
+ FileAccountImpl: Codeunit "File Account Impl.";
+ CanUserManageFileSetup: Boolean;
+ IsDefault: Boolean;
+ IsInLookupMode: Boolean;
+ UpdateAccounts: Boolean;
+ FileConnectorHasBeenUninstalledMsg: Label 'The selected file extension has been uninstalled. To view information about the file account, you must reinstall the extension.';
+ DefaultTxt: Text;
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Connector/ExtFileStorageConnector.Enum.al b/src/System Application/App/External File Storage/src/Connector/ExtFileStorageConnector.Enum.al
new file mode 100644
index 0000000000..fcb9daab3d
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Connector/ExtFileStorageConnector.Enum.al
@@ -0,0 +1,14 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Enum that holds all of the available file connectors.
+///
+enum 9450 "Ext. File Storage Connector" implements "External File Storage Connector"
+{
+ Extensible = true;
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Connector/ExtFileStorageConnector.Table.al b/src/System Application/App/External File Storage/src/Connector/ExtFileStorageConnector.Table.al
new file mode 100644
index 0000000000..eeb99a9b87
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Connector/ExtFileStorageConnector.Table.al
@@ -0,0 +1,29 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+table 9451 "Ext. File Storage Connector"
+{
+ TableType = Temporary;
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ fields
+ {
+ field(1; Connector; Enum "Ext. File Storage Connector") { }
+ field(2; Logo; Media) { }
+ field(3; Description; Text[250]) { }
+ }
+
+ keys
+ {
+ key(PK; Connector)
+ {
+ Clustered = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Connector/ExternalFileStorageConnector.Interface.al b/src/System Application/App/External File Storage/src/Connector/ExternalFileStorageConnector.Interface.al
new file mode 100644
index 0000000000..6f03b52a80
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Connector/ExternalFileStorageConnector.Interface.al
@@ -0,0 +1,140 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// A External File Storage Connector interface "used to create file accounts and handle external files."
+///
+interface "External File Storage Connector"
+{
+ ///
+ /// Gets a List of Files stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all files stored in the path.
+ procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary);
+
+ ///
+ /// Gets a file from the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read to.
+ procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream);
+
+ ///
+ /// Gets a file to the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// The Stream were the file is read from.
+ procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream);
+
+ ///
+ /// Copies as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text);
+
+ ///
+ /// Move as file inside the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The source file path.
+ /// The target file path.
+ procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text);
+
+ ///
+ /// Checks if a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ /// Returns true if the file exists
+ procedure FileExists(AccountId: Guid; Path: Text): Boolean;
+
+ ///
+ /// Deletes a file exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The file path inside the file account.
+ procedure DeleteFile(AccountId: Guid; Path: Text);
+
+ ///
+ /// Gets a List of Directories stored on the provided account.
+ ///
+ /// The file account ID which is used to get the file.
+ /// The file path to list.
+ /// Defines the pagination data.
+ /// A list with all directories stored in the path.
+ procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary);
+
+ ///
+ /// Creates a directory on the provided account.
+ ///
+ /// The directory path inside the file account.
+ /// The file account ID which is used to send out the file.
+ procedure CreateDirectory(AccountId: Guid; Path: Text);
+
+ ///
+ /// Checks if a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ /// Returns true if the directory exists
+ procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean;
+
+ ///
+ /// Deletes a directory exists on the provided account.
+ ///
+ /// The file account ID which is used to send out the file.
+ /// The directory path inside the file account.
+ procedure DeleteDirectory(AccountId: Guid; Path: Text);
+
+ ///
+ /// Gets the file accounts registered for the connector.
+ ///
+ /// Out variable that holds the registered file accounts for the connector.
+ procedure GetAccounts(var TempAccounts: Record "File Account" temporary);
+
+ ///
+ /// Shows the information for a file account.
+ ///
+ /// The ID of the file account
+ procedure ShowAccountInformation(AccountId: Guid);
+
+ ///
+ /// Registers a file account for the connector.
+ ///
+ /// The out parameter must hold the account ID of the added account.
+ /// Out parameter with the details of the registered Account.
+ /// True if an account was registered.
+ procedure RegisterAccount(var TempFileAccount: Record "File Account" temporary): Boolean
+
+ ///
+ /// Deletes a file account for the connector.
+ ///
+ /// The ID of the file account
+ /// True if an account was deleted.
+ procedure DeleteAccount(AccountId: Guid): Boolean
+
+ ///
+ /// Provides a custom logo for the connector that shows in the Setup File Account Guide.
+ ///
+ /// Base64 encoded image.
+ /// The recommended image size is 128x128.
+ /// The logo of the connector is Base64 format
+ procedure GetLogoAsBase64(): Text;
+
+ ///
+ /// Provides a more detailed description of the connector.
+ ///
+ /// A more detailed description of the connector.
+ procedure GetDescription(): Text[250];
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Connector/FileStorageConnectorLogo.Table.al b/src/System Application/App/External File Storage/src/Connector/FileStorageConnectorLogo.Table.al
new file mode 100644
index 0000000000..f3d324db71
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Connector/FileStorageConnectorLogo.Table.al
@@ -0,0 +1,28 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+table 9452 "File Storage Connector Logo"
+{
+ DataClassification = SystemMetadata;
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ fields
+ {
+ field(1; Connector; Enum "Ext. File Storage Connector") { }
+ field(2; Logo; Media) { }
+ }
+
+ keys
+ {
+ key(PK; Connector)
+ {
+ Clustered = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/FileStorage/ExternalFileStorage.Codeunit.al b/src/System Application/App/External File Storage/src/FileStorage/ExternalFileStorage.Codeunit.al
new file mode 100644
index 0000000000..c019ec6f7a
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/FileStorage/ExternalFileStorage.Codeunit.al
@@ -0,0 +1,249 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+codeunit 9454 "External File Storage"
+{
+ var
+ ExternalFileStorageImpl: Codeunit "External File Storage Impl.";
+
+ ///
+ /// Initialized the File Storage for the given scenario.
+ ///
+ /// File Scenario to use.
+ procedure Initialize(Scenario: Enum "File Scenario")
+ begin
+ ExternalFileStorageImpl.Initialize(Scenario);
+ end;
+
+ ///
+ /// Initialized the File Storage for the give file account.
+ ///
+ /// File Account to use.
+ procedure Initialize(TempFileAccount: Record "File Account" temporary)
+ begin
+ ExternalFileStorageImpl.Initialize(TempFileAccount);
+ end;
+
+ ///
+ /// List all files from the given path.
+ ///
+ /// Folder to list
+ /// Defines the pagination data.
+ /// File account content.
+ procedure ListFiles(Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ begin
+ ExternalFileStorageImpl.ListFiles(Path, FilePaginationData, TempFileAccountContent);
+ end;
+
+ ///
+ /// Retrieves a file from the file account.
+ ///
+ /// File Path to open.
+ /// Stream which contains the file content.
+ [TryFunction]
+ procedure GetFile(Path: Text; Stream: InStream)
+ begin
+ ExternalFileStorageImpl.GetFile(Path, Stream);
+ end;
+
+ ///
+ /// Stores a file in to the file account.
+ ///
+ /// File Path inside the file account.
+ /// Stream to store.
+ [TryFunction]
+ procedure CreateFile(Path: Text; Stream: InStream)
+ begin
+ ExternalFileStorageImpl.CreateFile(Path, Stream);
+ end;
+
+ ///
+ /// Copies a file in the file account.
+ ///
+ /// Source path of the file.
+ /// Target Path of the file copy.
+ [TryFunction]
+ procedure CopyFile(SourcePath: Text; TargetPath: Text)
+ begin
+ ExternalFileStorageImpl.CopyFile(SourcePath, TargetPath);
+ end;
+
+ ///
+ /// Moves a file in the file account.
+ ///
+ /// Source path of the file.
+ /// Target Path of the file.
+ [TryFunction]
+ procedure MoveFile(SourcePath: Text; TargetPath: Text)
+ begin
+ ExternalFileStorageImpl.MoveFile(SourcePath, TargetPath);
+ end;
+
+ ///
+ /// Checks if a specific file exists in the file account.
+ ///
+ /// File path to check.
+ /// Returns true if the file exists.
+ procedure FileExists(Path: Text): Boolean
+ begin
+ exit(ExternalFileStorageImpl.FileExists(Path));
+ end;
+
+ ///
+ /// Deletes a file from the file account.
+ ///
+ /// File path of the file to delete.
+ [TryFunction]
+ procedure DeleteFile(Path: Text)
+ begin
+ ExternalFileStorageImpl.DeleteFile(Path);
+ end;
+
+ ///
+ /// List all directories from the given path.
+ ///
+ /// Folder to list
+ /// Defines the pagination data.
+ /// File account content.
+ [TryFunction]
+ procedure ListDirectories(Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ begin
+ ExternalFileStorageImpl.ListDirectories(Path, FilePaginationData, TempFileAccountContent);
+ end;
+
+ ///
+ /// Creates a directory in the file account.
+ ///
+ /// Path of the new Directory to create.
+ [TryFunction]
+ procedure CreateDirectory(Path: Text)
+ begin
+ ExternalFileStorageImpl.CreateDirectory(Path);
+ end;
+
+ ///
+ /// Checks if a specific directory exists in the file account.
+ ///
+ /// Path of the directory to check.
+ /// Returns true if directory exists.
+ procedure DirectoryExists(Path: Text): Boolean
+ begin
+ exit(ExternalFileStorageImpl.DirectoryExists(Path));
+ end;
+
+ ///
+ /// Deletes a directory from the file account.
+ ///
+ /// Directory to remove.
+ [TryFunction]
+ procedure DeleteDirectory(Path: Text)
+ begin
+ ExternalFileStorageImpl.DeleteDirectory(Path);
+ end;
+
+ ///
+ /// Combines to paths together.
+ ///
+ /// First part to combine.
+ /// Second part to combine.
+ /// Correctly combined path.
+ procedure CombinePath(Path: Text; ChildPath: Text): Text
+ begin
+ exit(ExternalFileStorageImpl.CombinePath(Path, ChildPath));
+ end;
+
+ ///
+ /// Gets the Parent Path of the given path.
+ ///
+ /// File or directory path.
+ /// The parent of the specified path.
+ procedure GetParentPath(Path: Text): Text
+ begin
+ exit(ExternalFileStorageImpl.GetParentPath(Path));
+ end;
+
+ ///
+ /// Opens a folder selection dialog.
+ ///
+ /// Start path of the dialog.
+ /// Returns the selected Folder.
+ procedure SelectAndGetFolderPath(Path: Text): Text
+ var
+ DefaultSelectFolderLbl: Label 'Select a folder';
+ begin
+ exit(SelectAndGetFolderPath(Path, DefaultSelectFolderLbl));
+ end;
+
+ ///
+ /// Opens a folder selection dialog.
+ ///
+ /// Start path of the dialog.
+ /// Title of the selection dialog.
+ /// Returns the selected Folder.
+ procedure SelectAndGetFolderPath(Path: Text; DialogTitle: Text): Text
+ begin
+ exit(ExternalFileStorageImpl.SelectAndGetFolderPath(Path, DialogTitle));
+ end;
+
+ ///
+ /// Opens a select file dialog.
+ ///
+ /// Start path.
+ /// A filter string that applies only on files not on folders.
+ /// Returns the path of the selected file.
+ procedure SelectAndGetFilePath(Path: Text; FileFilter: Text): Text
+ var
+ DefaultSelectFileUILbl: Label 'Select a file';
+ begin
+ exit(SelectAndGetFilePath(Path, FileFilter, DefaultSelectFileUILbl));
+ end;
+
+ ///
+ /// Opens a select file dialog.
+ ///
+ /// Start path of the dialog.
+ /// A filter string that applies only on files not on folders.
+ /// Title of the selection dialog.
+ /// Returns the path of the selected file.
+ procedure SelectAndGetFilePath(Path: Text; FileFilter: Text; DialogTitle: Text): Text
+ begin
+ exit(ExternalFileStorageImpl.SelectAndGetFilePath(Path, FileFilter, DialogTitle));
+ end;
+
+ ///
+ /// Opens a save to dialog.
+ ///
+ /// Start path of the dialog.
+ /// The file extension without dot (like pdf or txt).
+ /// Returns the selected file path.
+ procedure SaveFile(Path: Text; FileExtension: Text): Text
+ var
+ DefaultSaveFileTitleLbl: Label 'Save as';
+ begin
+ exit(SaveFile(Path, FileExtension, DefaultSaveFileTitleLbl));
+ end;
+
+ ///
+ /// Opens a save to dialog.
+ ///
+ /// Start path of the dialog.
+ /// The file extension without dot (like pdf or txt).
+ /// Title of the selection dialog.
+ /// Returns the selected file path.
+ procedure SaveFile(Path: Text; FileExtension: Text; DialogTitle: Text): Text
+ begin
+ exit(ExternalFileStorageImpl.SaveFile(Path, FileExtension, DialogTitle));
+ end;
+
+ ///
+ /// Opens a File Browser
+ ///
+ procedure BrowseAccount()
+ begin
+ ExternalFileStorageImpl.BrowseAccount();
+ end;
+}
diff --git a/src/System Application/App/External File Storage/src/FileStorage/ExternalFileStorageImpl.Codeunit.al b/src/System Application/App/External File Storage/src/FileStorage/ExternalFileStorageImpl.Codeunit.al
new file mode 100644
index 0000000000..636901d104
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/FileStorage/ExternalFileStorageImpl.Codeunit.al
@@ -0,0 +1,249 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+codeunit 9455 "External File Storage Impl."
+{
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ var
+ TempCurrFileAccount: Record "File Account" temporary;
+ FileSystemConnector: Interface "External File Storage Connector";
+ IsInitialized: Boolean;
+
+ procedure Initialize(Scenario: Enum "File Scenario")
+ var
+ TempFileAccount: Record "File Account" temporary;
+ FileScenarioMgt: Codeunit "File Scenario";
+ NoFileAccountFoundErr: Label 'No default file account defined.';
+ begin
+ if not FileScenarioMgt.GetFileAccount(Scenario, TempFileAccount) then
+ Error(NoFileAccountFoundErr);
+
+ Initialize(TempFileAccount);
+ end;
+
+ procedure Initialize(TempFileAccount: Record "File Account" temporary)
+ begin
+ TempCurrFileAccount := TempFileAccount;
+ FileSystemConnector := TempFileAccount.Connector;
+ IsInitialized := true;
+ end;
+
+ procedure ListFiles(Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ FileSystemConnector.ListFiles(TempCurrFileAccount."Account Id", Path, FilePaginationData, TempFileAccountContent);
+ end;
+
+ procedure GetFile(Path: Text; Stream: InStream)
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ FileSystemConnector.GetFile(TempCurrFileAccount."Account Id", Path, Stream);
+ end;
+
+ procedure CreateFile(Path: Text; Stream: InStream)
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ FileSystemConnector.CreateFile(TempCurrFileAccount."Account Id", Path, Stream);
+ end;
+
+ procedure CopyFile(SourcePath: Text; TargetPath: Text)
+ begin
+ CheckInitialization();
+ CheckPath(SourcePath);
+ CheckPath(TargetPath);
+ FileSystemConnector.CopyFile(TempCurrFileAccount."Account Id", SourcePath, TargetPath);
+ end;
+
+ procedure MoveFile(SourcePath: Text; TargetPath: Text)
+ begin
+ CheckInitialization();
+ CheckPath(SourcePath);
+ CheckPath(TargetPath);
+ FileSystemConnector.MoveFile(TempCurrFileAccount."Account Id", SourcePath, TargetPath);
+ end;
+
+ procedure FileExists(Path: Text): Boolean
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ exit(FileSystemConnector.FileExists(TempCurrFileAccount."Account Id", Path));
+ end;
+
+ procedure DeleteFile(Path: Text)
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ FileSystemConnector.DeleteFile(TempCurrFileAccount."Account Id", Path);
+ end;
+
+ procedure ListDirectories(Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary)
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ FileSystemConnector.ListDirectories(TempCurrFileAccount."Account Id", Path, FilePaginationData, TempFileAccountContent);
+ end;
+
+ procedure CreateDirectory(Path: Text)
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ FileSystemConnector.CreateDirectory(TempCurrFileAccount."Account Id", Path);
+ end;
+
+ procedure DirectoryExists(Path: Text): Boolean
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ exit(FileSystemConnector.DirectoryExists(TempCurrFileAccount."Account Id", Path));
+ end;
+
+ procedure DeleteDirectory(Path: Text)
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+ FileSystemConnector.DeleteDirectory(TempCurrFileAccount."Account Id", Path);
+ end;
+
+ procedure PathSeparator(): Text
+ begin
+ exit('/');
+ end;
+
+ procedure CombinePath(Path: Text; ChildPath: Text): Text
+ begin
+ if Path = '' then
+ exit(ChildPath);
+
+ if not Path.EndsWith(PathSeparator()) then
+ Path += PathSeparator();
+
+ exit(Path + ChildPath);
+ end;
+
+ procedure GetParentPath(Path: Text) ParentPath: Text
+ begin
+ Path := Path.TrimEnd(PathSeparator());
+ if Path.TrimEnd(PathSeparator()).Contains(PathSeparator()) then
+ ParentPath := Path.Substring(1, Path.LastIndexOf(PathSeparator()));
+ end;
+
+ procedure SelectAndGetFolderPath(Path: Text; DialogTitle: Text): Text
+ var
+ TempFileAccountContent: Record "File Account Content" temporary;
+ StorageBrowser: Page "Storage Browser";
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+
+ StorageBrowser.SetPageCaption(DialogTitle);
+ StorageBrowser.SetFileAccount(TempCurrFileAccount);
+ StorageBrowser.EnableDirectoryLookupMode(Path);
+ if StorageBrowser.RunModal() <> Action::LookupOK then
+ exit('');
+
+ StorageBrowser.GetRecord(TempFileAccountContent);
+ if TempFileAccountContent.Type <> TempFileAccountContent.Type::Directory then
+ exit('');
+
+ exit(CombinePath(TempFileAccountContent."Parent Directory", TempFileAccountContent.Name));
+ end;
+
+ procedure SelectAndGetFilePath(Path: Text; FileFilter: Text; DialogTitle: Text): Text
+ var
+ TempFileAccountContent: Record "File Account Content" temporary;
+ StorageBrowser: Page "Storage Browser";
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+
+ StorageBrowser.SetPageCaption(DialogTitle);
+ StorageBrowser.SetFileAccount(TempCurrFileAccount);
+ StorageBrowser.EnableFileLookupMode(Path, FileFilter);
+ if StorageBrowser.RunModal() <> Action::LookupOK then
+ exit('');
+
+ StorageBrowser.GetRecord(TempFileAccountContent);
+ if TempFileAccountContent.Type <> TempFileAccountContent.Type::File then
+ exit('');
+
+ exit(CombinePath(TempFileAccountContent."Parent Directory", TempFileAccountContent.Name));
+ end;
+
+ procedure SaveFile(Path: Text; FileExtension: Text; DialogTitle: Text): Text
+ var
+ StorageBrowser: Page "Storage Browser";
+ FileName, FileNameWithExtension : Text;
+ PleaseProvideFileExtensionErr: Label 'Please provide a valid file extension.';
+ FileNameTok: Label '%1.%2', Locked = true;
+ begin
+ CheckInitialization();
+ CheckPath(Path);
+
+ if FileExtension = '' then
+ Error(PleaseProvideFileExtensionErr);
+
+ StorageBrowser.SetPageCaption(DialogTitle);
+ StorageBrowser.SetFileAccount(TempCurrFileAccount);
+ StorageBrowser.EnableSaveFileLookupMode(Path, FileExtension);
+ if StorageBrowser.RunModal() <> Action::LookupOK then
+ exit('');
+
+ FileName := StorageBrowser.GetFileName();
+ if FileName = '' then
+ exit('');
+
+ FileNameWithExtension := StrSubstNo(FileNameTok, FileName, FileExtension);
+ exit(CombinePath(StorageBrowser.GetCurrentDirectory(), FileNameWithExtension));
+ end;
+
+ procedure BrowseAccount()
+ var
+ FileAccountImpl: Codeunit "File Account Impl.";
+ begin
+ CheckInitialization();
+ FileAccountImpl.BrowseAccount(TempCurrFileAccount);
+ end;
+
+ local procedure CheckInitialization()
+ var
+ NotInitializedErr: Label 'Please call Initialize() first.';
+ begin
+ if IsInitialized then
+ exit;
+
+ Error(NotInitializedErr);
+ end;
+
+ local procedure CheckPath(Path: Text)
+ var
+ InvalidChar: Char;
+ PathCannotStartWithSlashErr: Label 'The path %1 can not start with /.', Comment = '%1 - Path';
+ InvalidChars: Text;
+
+ begin
+ if Path.StartsWith('/') then
+ Error(PathCannotStartWithSlashErr, Path);
+
+ InvalidChars := '"''<>\|';
+ foreach InvalidChar in InvalidChars do
+ CheckPath(Path, InvalidChar);
+ end;
+
+ local procedure CheckPath(Path: Text; InvalidChar: Char)
+ var
+ InvalidPathErr: Label 'The path %1 contains the invalid character %2.', Comment = '%1 - Path, %2 - Invalid Character';
+ begin
+ if Path.Contains(InvalidChar) then
+ Error(InvalidPathErr, Path, InvalidChar);
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Lookup/ExtFileStorageFileType.Enum.al b/src/System Application/App/External File Storage/src/Lookup/ExtFileStorageFileType.Enum.al
new file mode 100644
index 0000000000..b5aaf26b1f
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Lookup/ExtFileStorageFileType.Enum.al
@@ -0,0 +1,31 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Indicator of what type the resource is.
+///
+enum 9452 "Ext. File Storage File Type"
+{
+ Access = Public;
+ Extensible = false;
+
+ ///
+ /// Indicates if entry is a directory.
+ ///
+ value(0; Directory)
+ {
+ Caption = 'Directory';
+ }
+
+ ///
+ /// Indicates if entry is a file type.
+ ///
+ value(1; File)
+ {
+ Caption = 'File';
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Lookup/FileAccountBrowserMgt.Codeunit.al b/src/System Application/App/External File Storage/src/Lookup/FileAccountBrowserMgt.Codeunit.al
new file mode 100644
index 0000000000..46fc164795
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Lookup/FileAccountBrowserMgt.Codeunit.al
@@ -0,0 +1,134 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+codeunit 9458 "File Account Browser Mgt."
+{
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ var
+ ExternalFileStorage: Codeunit "External File Storage";
+
+ procedure SetFileAccount(TempFileAccount: Record "File Account" temporary)
+ begin
+ ExternalFileStorage.Initialize(TempFileAccount);
+ end;
+
+ procedure StripNotSupportedCharsInFileName(InText: Text): Text
+ var
+ InvalidCharsStringTxt: Label '"#%&*:<>?\/{|}~', Locked = true;
+ begin
+ InText := DelChr(InText, '=', InvalidCharsStringTxt);
+ exit(InText);
+ end;
+
+ procedure BrowseFolder(var TempFileAccountContent: Record "File Account Content" temporary; Path: Text; var CurrentPath: Text; DoNotLoadFiles: Boolean; FileNameFilter: Text)
+ var
+ FilePaginationData: Codeunit "File Pagination Data";
+ begin
+ CurrentPath := Path.TrimEnd('/');
+ TempFileAccountContent.DeleteAll();
+
+ repeat
+ ExternalFileStorage.ListDirectories(Path, FilePaginationData, TempFileAccountContent);
+ until FilePaginationData.IsEndOfListing();
+
+ ListFiles(TempFileAccountContent, Path, DoNotLoadFiles, CurrentPath, FileNameFilter);
+ if TempFileAccountContent.FindFirst() then;
+ end;
+
+ procedure DownloadFile(var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ Stream: InStream;
+ FileName: Text;
+ begin
+ ExternalFileStorage.GetFile(ExternalFileStorage.CombinePath(TempFileAccountContent."Parent Directory", TempFileAccountContent.Name), Stream);
+ FileName := TempFileAccountContent.Name;
+ DownloadFromStream(Stream, '', '', '', FileName);
+ end;
+
+ procedure UploadFile(Path: Text)
+ var
+ Stream: InStream;
+ UploadDialogTxt: Label 'Upload File';
+ FromFile: Text;
+ begin
+ if not UploadIntoStream(UploadDialogTxt, '', '', FromFile, Stream) then
+ exit;
+
+ ExternalFileStorage.CreateFile(ExternalFileStorage.CombinePath(Path, FromFile), Stream);
+ end;
+
+ procedure CreateDirectory(Path: Text)
+ var
+ FolderNameInput: Page "Folder Name Input";
+ FolderName: Text;
+ begin
+ if FolderNameInput.RunModal() <> Action::OK then
+ exit;
+
+ FolderName := StripNotSupportedCharsInFileName(FolderNameInput.GetFolderName());
+ ExternalFileStorage.CreateDirectory(ExternalFileStorage.CombinePath(Path, FolderName));
+ end;
+
+ local procedure ListFiles(var TempFileAccountContent: Record "File Account Content" temporary; Path: Text; DoNotLoadFields: Boolean; CurrentPath: Text; FileNameFilter: Text)
+ var
+ TempFileAccountContentToAdd: Record "File Account Content" temporary;
+ FilePaginationData: Codeunit "File Pagination Data";
+ begin
+ if DoNotLoadFields then
+ exit;
+
+ repeat
+ ExternalFileStorage.ListFiles(Path, FilePaginationData, TempFileAccountContent);
+ until FilePaginationData.IsEndOfListing();
+
+ AddFiles(TempFileAccountContent, TempFileAccountContentToAdd, CurrentPath, FileNameFilter);
+ end;
+
+ local procedure AddFiles(var TempFileAccountContent: Record "File Account Content" temporary; var FileAccountContentToAdd: Record "File Account Content" temporary; CurrentPath: Text; FileNameFilter: Text)
+ begin
+ if FileNameFilter <> '' then
+ FileAccountContentToAdd.SetFilter(Name, FileNameFilter);
+
+ if FileAccountContentToAdd.FindSet() then
+ repeat
+ TempFileAccountContent.Init();
+ TempFileAccountContent.TransferFields(FileAccountContentToAdd);
+ TempFileAccountContent.Insert();
+ until FileAccountContentToAdd.Next() = 0;
+
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Validate(Name, '..');
+ TempFileAccountContent.Validate(Type, TempFileAccountContent.Type::Directory);
+ TempFileAccountContent.Validate("Parent Directory", CopyStr(ExternalFileStorage.GetParentPath(CurrentPath), 1, MaxStrLen(TempFileAccountContent."Parent Directory")));
+ TempFileAccountContent.Insert();
+ end;
+
+ procedure DeleteFileOrDirectory(var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ DeleteQst: Label 'Delete %1?', Comment = '%1 - Path to Delete';
+ PathToDelete: Text;
+ begin
+ PathToDelete := ExternalFileStorage.CombinePath(TempFileAccountContent."Parent Directory", TempFileAccountContent.Name);
+ if not Confirm(DeleteQst, false, PathToDelete) then
+ exit;
+
+ case TempFileAccountContent.Type of
+ TempFileAccountContent.Type::Directory:
+ ExternalFileStorage.DeleteDirectory(PathToDelete);
+ TempFileAccountContent.Type::File:
+ ExternalFileStorage.DeleteFile(PathToDelete);
+ end;
+ end;
+
+ internal procedure CombinePath(Path: Text; ChildPath: Text): Text
+ begin
+ exit(ExternalFileStorage.CombinePath(Path, ChildPath));
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Lookup/FileAccountContent.Table.al b/src/System Application/App/External File Storage/src/Lookup/FileAccountContent.Table.al
new file mode 100644
index 0000000000..05fcdeca5a
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Lookup/FileAccountContent.Table.al
@@ -0,0 +1,37 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+table 9455 "File Account Content"
+{
+ Caption = 'File Account Content';
+ DataClassification = SystemMetadata;
+ TableType = Temporary;
+ Extensible = false;
+
+ fields
+ {
+ field(1; "Type"; Enum "Ext. File Storage File Type")
+ {
+ Caption = 'Type';
+ }
+ field(2; Name; Text[2048])
+ {
+ Caption = 'Name';
+ }
+ field(10; "Parent Directory"; Text[2048])
+ {
+ Caption = 'Parent Directory';
+ }
+ }
+ keys
+ {
+ key(PK; "Type", Name)
+ {
+ Clustered = true;
+ }
+ }
+}
diff --git a/src/System Application/App/External File Storage/src/Lookup/FilePaginationData.Codeunit.al b/src/System Application/App/External File Storage/src/Lookup/FilePaginationData.Codeunit.al
new file mode 100644
index 0000000000..31a83ee026
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Lookup/FilePaginationData.Codeunit.al
@@ -0,0 +1,48 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+codeunit 9456 "File Pagination Data"
+{
+ var
+ FilePaginationDataImpl: Codeunit "File Pagination Data Impl.";
+
+ ///
+ /// Sets a marker to see if files and directories can be retrieved in batches.
+ ///
+ /// Marker value to set.
+ procedure SetMarker(NewMarker: Text)
+ begin
+ FilePaginationDataImpl.SetMarker(NewMarker);
+ end;
+
+ ///
+ /// Gets the current marker value.
+ ///
+ /// Current marker value.
+ procedure GetMarker(): Text
+ begin
+ exit(FilePaginationDataImpl.GetMarker());
+ end;
+
+ ///
+ /// Set this value to true, if all files or directories have been read a from the File Service.
+ ///
+ /// End of listing reached.
+ procedure SetEndOfListing(NewEndOfListing: Boolean)
+ begin
+ FilePaginationDataImpl.SetEndOfListing(NewEndOfListing);
+ end;
+
+ ///
+ /// Defines if all batches of directory or file listing has been received.
+ ///
+ /// End of listing reached.
+ procedure IsEndOfListing(): Boolean
+ begin
+ exit(FilePaginationDataImpl.IsEndOfListing());
+ end;
+}
diff --git a/src/System Application/App/External File Storage/src/Lookup/FilePaginationDataImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Lookup/FilePaginationDataImpl.Codeunit.al
new file mode 100644
index 0000000000..8eae632206
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Lookup/FilePaginationDataImpl.Codeunit.al
@@ -0,0 +1,37 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+codeunit 9457 "File Pagination Data Impl."
+{
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ var
+ EndOfListing: Boolean;
+ Marker: Text;
+
+ procedure SetMarker(NewMarker: Text)
+ begin
+ Marker := NewMarker;
+ end;
+
+ procedure GetMarker(): Text
+ begin
+ exit(Marker);
+ end;
+
+ procedure SetEndOfListing(NewEndOfListing: Boolean)
+ begin
+ EndOfListing := NewEndOfListing;
+ end;
+
+ procedure IsEndOfListing(): Boolean
+ begin
+ exit(EndOfListing);
+ end;
+}
diff --git a/src/System Application/App/External File Storage/src/Lookup/FolderNameInput.Page.al b/src/System Application/App/External File Storage/src/Lookup/FolderNameInput.Page.al
new file mode 100644
index 0000000000..be6e13fa9c
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Lookup/FolderNameInput.Page.al
@@ -0,0 +1,36 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+page 9456 "Folder Name Input"
+{
+ ApplicationArea = All;
+ Caption = 'Create Folder...';
+ PageType = StandardDialog;
+ Extensible = false;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ layout
+ {
+ area(Content)
+ {
+ field(FolderNameField; FolderName)
+ {
+ Caption = 'Folder Name';
+ ToolTip = 'Specifies the Name of the directory.';
+ }
+ }
+ }
+
+ var
+ FolderName: Text;
+
+ internal procedure GetFolderName(): Text
+ begin
+ exit(FolderName);
+ end;
+}
diff --git a/src/System Application/App/External File Storage/src/Lookup/StorageBrowser.Page.al b/src/System Application/App/External File Storage/src/Lookup/StorageBrowser.Page.al
new file mode 100644
index 0000000000..c272242598
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Lookup/StorageBrowser.Page.al
@@ -0,0 +1,207 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+page 9455 "Storage Browser"
+{
+ Caption = 'Storage Browser';
+ PageType = List;
+ ApplicationArea = All;
+ SourceTable = "File Account Content";
+ ModifyAllowed = false;
+ InsertAllowed = false;
+ DeleteAllowed = false;
+ Extensible = false;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+ UsageCategory = None;
+
+ layout
+ {
+ area(Content)
+ {
+ field(CurrentPathField; CurrentPath)
+ {
+ Caption = 'Path';
+ ShowCaption = false;
+ Editable = false;
+ }
+ repeater(General)
+ {
+ field(Name; Rec.Name)
+ {
+ DrillDown = true;
+ ToolTip = 'Specifies the value of the Name field.';
+
+ trigger OnDrillDown()
+ begin
+ case true of
+ Rec.Name = '..':
+ BrowseFolder(Rec."Parent Directory");
+ Rec.Type = Rec.Type::Directory:
+ BrowseFolder(Rec);
+ not IsInLookupMode:
+ FileAccountBrowserMgt.DownloadFile(Rec);
+ end;
+ end;
+ }
+ field("Type"; Rec."Type")
+ {
+ ToolTip = 'Specifies the value of the Type field.';
+ }
+ }
+
+ group(SaveFileNameGroup)
+ {
+ ShowCaption = false;
+ Visible = ShowFileName;
+
+ field(SaveFileNameField; SaveFileName)
+ {
+ Caption = 'Filename';
+ ToolTip = 'Specifies the Name of the File.';
+ }
+ }
+ }
+ }
+
+ actions
+ {
+ area(Promoted)
+ {
+ actionref(UploadRef; Upload) { }
+ actionref(CreateDirectoryRef; "Create Directory") { }
+ actionref(DeleteRef; Delete) { }
+ }
+ area(Processing)
+ {
+ action(Upload)
+ {
+ Caption = 'Upload';
+ Image = Import;
+ Ellipsis = true;
+ Visible = not IsInLookupMode;
+ Enabled = not IsInLookupMode;
+ ToolTip = 'Uploads a file to the current directory.';
+
+ trigger OnAction()
+ begin
+ FileAccountBrowserMgt.UploadFile(CurrentPath);
+ BrowseFolder(CurrentPath);
+ end;
+ }
+ action("Create Directory")
+ {
+ Caption = 'Create Directory';
+ Image = Bin;
+ Ellipsis = true;
+ Visible = not IsInLookupMode;
+ Enabled = not IsInLookupMode;
+ ToolTip = 'Creates a new directory in the current directory.';
+
+ trigger OnAction()
+ begin
+ FileAccountBrowserMgt.CreateDirectory(CurrentPath);
+ BrowseFolder(CurrentPath);
+ end;
+ }
+ action(Delete)
+ {
+ Caption = 'Delete';
+ Image = Delete;
+ Ellipsis = true;
+ Visible = not IsInLookupMode;
+ Enabled = not IsInLookupMode;
+ ToolTip = 'Deletes the selected file or directory.';
+
+ trigger OnAction()
+ begin
+ FileAccountBrowserMgt.DeleteFileOrDirectory(Rec);
+ BrowseFolder(CurrentPath);
+ end;
+ }
+ }
+ }
+
+ var
+ FileAccountBrowserMgt: Codeunit "File Account Browser Mgt.";
+ CurrentPath, FileFilter, SaveFileName, CurrentPageCaption : Text;
+ DoNotLoadFiles, IsInLookupMode, ShowFileName : Boolean;
+
+ trigger OnOpenPage()
+ begin
+ if CurrentPageCaption <> '' then
+ CurrPage.Caption(CurrentPageCaption);
+ end;
+
+ internal procedure SetFileAccount(TempFileAccount: Record "File Account" temporary)
+ begin
+ FileAccountBrowserMgt.SetFileAccount(TempFileAccount);
+ end;
+
+ internal procedure BrowseFileAccount(Path: Text)
+ begin
+ BrowseFolder('');
+ end;
+
+ internal procedure EnableFileLookupMode(Path: Text; PassedFileFilter: Text)
+ begin
+ FileFilter := PassedFileFilter;
+ EnableLookupMode();
+ BrowseFolder(Path);
+ end;
+
+ internal procedure EnableDirectoryLookupMode(Path: Text)
+ begin
+ DoNotLoadFiles := true;
+ EnableLookupMode();
+ BrowseFolder(Path);
+ end;
+
+ internal procedure EnableSaveFileLookupMode(Path: Text; FileExtension: Text)
+ var
+ FileFilterTok: Label '*.%1', Locked = true;
+ begin
+ ShowFileName := true;
+ FileFilter := StrSubstNo(FileFilterTok, FileExtension);
+ EnableLookupMode();
+ BrowseFolder(Path);
+ end;
+
+ internal procedure GetCurrentDirectory(): Text
+ begin
+ exit(CurrentPath);
+ end;
+
+ internal procedure GetFileName(): Text
+ begin
+ exit(SaveFileName);
+ end;
+
+ internal procedure SetPageCaption(NewCaption: Text)
+ begin
+ CurrentPageCaption := NewCaption;
+ end;
+
+ local procedure EnableLookupMode()
+ begin
+ IsInLookupMode := true;
+ CurrPage.LookupMode(true);
+ end;
+
+ local procedure BrowseFolder(var TempFileAccountContent: Record "File Account Content" temporary)
+ var
+ Path: Text;
+ begin
+ Path := FileAccountBrowserMgt.CombinePath(TempFileAccountContent."Parent Directory", TempFileAccountContent.Name);
+ BrowseFolder(Path);
+ end;
+
+ local procedure BrowseFolder(Path: Text)
+ begin
+ FileAccountBrowserMgt.BrowseFolder(Rec, Path, CurrentPath, DoNotLoadFiles, FileFilter);
+ end;
+}
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileAccountEntryType.Enum.al b/src/System Application/App/External File Storage/src/Scenario/FileAccountEntryType.Enum.al
new file mode 100644
index 0000000000..fdc1120936
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileAccountEntryType.Enum.al
@@ -0,0 +1,15 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+enum 9453 "File Account Entry Type"
+{
+ Access = Internal;
+ Extensible = false;
+
+ value(0; Account) { }
+ value(1; Scenario) { }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileAccountScenario.Table.al b/src/System Application/App/External File Storage/src/Scenario/FileAccountScenario.Table.al
new file mode 100644
index 0000000000..65a28fd84f
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileAccountScenario.Table.al
@@ -0,0 +1,43 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Temporary table used to display the tree structure in "File Scenario Setup".
+///
+table 9453 "File Account Scenario"
+{
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+ TableType = Temporary;
+
+ fields
+ {
+ field(1; Scenario; Integer) { }
+ field(2; Connector; Enum "Ext. File Storage Connector") { }
+ field(3; "Account Id"; Guid) { }
+ field(4; "Display Name"; Text[2048]) { }
+ field(5; Default; Boolean) { }
+ field(6; EntryType; Enum "File Account Entry Type") { }
+ field(7; Position; Integer) { }
+ }
+
+ keys
+ {
+ key(PK; Scenario, "Account Id", Connector)
+ {
+ Clustered = true;
+ }
+ key(Position; Position)
+ {
+ }
+ key(Name; "Display Name")
+ {
+ Description = 'Used for sorting by Display Name';
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
new file mode 100644
index 0000000000..82200e7283
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Codeunit.al
@@ -0,0 +1,76 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Provides functionality to work with file scenarios.
+///
+codeunit 9452 "File Scenario"
+{
+ ///
+ /// Gets the default file account.
+ ///
+ /// Out parameter holding information about the default file account.
+ /// True if an account for the default scenario was found; otherwise - false.
+ procedure GetDefaultFileAccount(var TempFileAccount: Record "File Account" temporary): Boolean
+ begin
+ exit(FileScenarioImpl.GetFileAccount(Enum::"File Scenario"::Default, TempFileAccount));
+ end;
+
+ ///
+ /// Gets the file account used by the given file scenario.
+ /// If the no account is defined for the provided scenario, the default account (if defined) will be returned.
+ ///
+ /// The scenario to look for.
+ /// Out parameter holding information about the file account.
+ /// True if an account for the specified scenario was found; otherwise - false.
+ procedure GetFileAccount(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
+ begin
+ exit(FileScenarioImpl.GetFileAccount(Scenario, TempFileAccount));
+ end;
+
+ ///
+ /// Sets a default file account.
+ ///
+ /// The file account to use.
+ procedure SetDefaultFileAccount(TempFileAccount: Record "File Account" temporary)
+ begin
+ FileScenarioImpl.SetFileAccount(Enum::"File Scenario"::Default, TempFileAccount);
+ end;
+
+ ///
+ /// Sets a file account to be used by the given file scenario.
+ ///
+ /// The scenario for which to set a file account.
+ /// The file account to use.
+ procedure SetFileAccount(Scenario: Enum "File Scenario"; TempFileAccount: Record "File Account" temporary)
+ begin
+ FileScenarioImpl.SetFileAccount(Scenario, TempFileAccount);
+ end;
+
+ ///
+ /// Unassign an file scenario. The scenario will then use the default file account.
+ ///
+ /// The scenario to unassign.
+ procedure UnassignScenario(Scenario: Enum "File Scenario")
+ begin
+ FileScenarioImpl.UnassignScenario(Scenario);
+ end;
+
+ ///
+ /// Event for changing whether an file scenario should be added to the list of assignable scenarios.
+ /// If the scenario has already been assigned or is the default scenario, this event won't be published.
+ ///
+ /// The scenario that is going to be added to the list of assignable scenarios.
+ /// The return for whether this scenario should be listed in the assignable scenarios list.
+ [IntegrationEvent(false, false, true)]
+ internal procedure OnBeforeInsertAvailableFileScenario(Scenario: Enum "File Scenario"; var IsAvailable: Boolean)
+ begin
+ end;
+
+ var
+ FileScenarioImpl: Codeunit "File Scenario Impl.";
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Enum.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Enum.al
new file mode 100644
index 0000000000..381e03b4ad
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Enum.al
@@ -0,0 +1,24 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// File scenarios.
+/// Used to decouple file accounts from sending files.
+///
+enum 9451 "File Scenario"
+{
+ Extensible = true;
+
+ ///
+ /// The default file scenario.
+ /// Used in the cases where no other scenario is defined.
+ ///
+ value(0; Default)
+ {
+ Caption = 'Default';
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenario.Table.al b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Table.al
new file mode 100644
index 0000000000..2f04ea1742
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenario.Table.al
@@ -0,0 +1,34 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Holds the mapping between file account and scenarios.
+/// One scenarios is mapped to one file account.
+/// One file account can be used for multiple scenarios.
+///
+table 9454 "File Scenario"
+{
+ DataClassification = SystemMetadata;
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ fields
+ {
+ field(1; Scenario; Enum "File Scenario") { }
+ field(2; Connector; Enum "Ext. File Storage Connector") { }
+ field(3; "Account Id"; Guid) { }
+ }
+
+ keys
+ {
+ key(PK; Scenario)
+ {
+ Clustered = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
new file mode 100644
index 0000000000..a4903091fb
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioImpl.Codeunit.al
@@ -0,0 +1,326 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+using System;
+
+codeunit 9453 "File Scenario Impl."
+{
+ Access = Internal;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+ Permissions = tabledata "File Scenario" = rimd;
+
+ procedure GetFileAccount(Scenario: Enum "File Scenario"; var TempFileAccount: Record "File Account" temporary): Boolean
+ var
+ TempAllFileAccounts: Record "File Account" temporary;
+ FileScenario: Record "File Scenario";
+ FileAccounts: Codeunit "File Account";
+ begin
+ FileAccounts.GetAllAccounts(TempAllFileAccounts);
+
+ // Find the account for the provided scenario
+ if FileScenario.Get(Scenario) then
+ if TempAllFileAccounts.Get(FileScenario."Account Id", FileScenario.Connector) then begin
+ TempFileAccount := TempAllFileAccounts;
+ exit(true);
+ end;
+
+ // Fallback to the default account if the scenario isn't mapped or the mapped account doesn't exist
+ if FileScenario.Get(Enum::"File Scenario"::Default) then
+ if TempAllFileAccounts.Get(FileScenario."Account Id", FileScenario.Connector) then begin
+ TempFileAccount := TempAllFileAccounts;
+ exit(true);
+ end;
+ end;
+
+ procedure SetFileAccount(Scenario: Enum "File Scenario"; TempFileAccount: Record "File Account" temporary)
+ var
+ FileScenario: Record "File Scenario";
+ begin
+ if not FileScenario.Get(Scenario) then begin
+ FileScenario.Scenario := Scenario;
+ FileScenario.Insert();
+ end;
+
+ FileScenario."Account Id" := TempFileAccount."Account Id";
+ FileScenario.Connector := TempFileAccount.Connector;
+
+ FileScenario.Modify();
+ end;
+
+ procedure UnassignScenario(Scenario: Enum "File Scenario")
+ var
+ FileScenario: Record "File Scenario";
+ begin
+ if FileScenario.Get(Scenario) then
+ FileScenario.Delete();
+ end;
+
+ ///
+ /// Get a list of entries, representing a tree structure with file accounts and the scenarios, assigned to each account.
+ ///
+ ///
+ /// Account sales@cronus.com has scenarios "Sales Quote" and "Sales Credit Memo" assigned.
+ /// Account purchase@cronus.com has scenarios "Purchase Quote" and "Purchase Invoice" assigned.
+ /// The result of calling the function will be:
+ /// sales@cronus.com, "Sales Quote", "Sales Credit Memo", purchase@cronus.com, "Purchase Quote", "Purchase Invoice"
+ ///
+ /// A flattened tree structure representing all the file accounts and the scenarios assigned to them.
+ procedure GetScenariosByFileAccount(var TempFileAccountScenario: Record "File Account Scenario" temporary)
+ var
+ TempDefaultFileAccount: Record "File Account" temporary;
+ TempFileAccounts: Record "File Account" temporary;
+ TempFileAccountScenarios: Record "File Account Scenario" temporary;
+ FileAccount: Codeunit "File Account";
+ Default: Boolean;
+ Position: Integer;
+ DisplayName: Text[2048];
+ begin
+ TempFileAccountScenario.Reset();
+ TempFileAccountScenario.DeleteAll();
+
+ FileAccount.GetAllAccounts(TempFileAccounts);
+
+ if not TempFileAccounts.FindSet() then
+ exit; // No accounts, nothing to do
+
+ // The position is set in order to be able to properly sort the entries (by order of insertion)
+ Position := 1;
+ GetDefaultAccount(TempDefaultFileAccount);
+
+ repeat
+ Default := (TempFileAccounts."Account Id" = TempDefaultFileAccount."Account Id") and (TempFileAccounts.Connector = TempDefaultFileAccount.Connector);
+ DisplayName := TempFileAccounts.Name;
+
+ // Add entry for the file account. Scenario is -1, because it isn't needed when displaying the file account.
+ AddEntry(TempFileAccountScenario, TempFileAccountScenario.EntryType::Account, -1, TempFileAccounts."Account Id", TempFileAccounts.Connector, DisplayName, Default, Position);
+
+ // Get the file scenarios assigned to the current file account, sorted by "Display Name"
+ GetFileScenariosForAccount(TempFileAccounts, TempFileAccountScenarios);
+
+ if TempFileAccountScenarios.FindSet() then
+ repeat
+ // Add entry for every scenario that is assigned to the current file account
+ AddEntry(TempFileAccountScenario, TempFileAccountScenarios.EntryType::Scenario, TempFileAccountScenarios.Scenario, TempFileAccountScenarios."Account Id", TempFileAccountScenarios.Connector, TempFileAccountScenarios."Display Name", false, Position);
+ until TempFileAccountScenarios.Next() = 0;
+ until TempFileAccounts.Next() = 0;
+
+ // Order by position to show accurate results
+ TempFileAccountScenario.SetCurrentKey(Position);
+ end;
+
+ local procedure GetFileScenariosForAccount(TempFileAccount: Record "File Account" temporary; var TempFileAccountScenarios: Record "File Account Scenario" temporary)
+ var
+ FileScenarios: Record "File Scenario";
+ ValidFileScenarios: DotNet Hashtable;
+ IsScenarioValid: Boolean;
+ Scenario: Integer;
+ begin
+ TempFileAccountScenarios.Reset();
+ TempFileAccountScenarios.DeleteAll();
+
+ // Get all file scenarios assigned to the file account
+ FileScenarios.SetRange("Account Id", TempFileAccount."Account Id");
+ FileScenarios.SetRange(Connector, TempFileAccount.Connector);
+
+ if not FileScenarios.FindSet() then
+ exit;
+
+ // Find all valid scenarios. Invalid scenario may occur if the extension that added them was removed.
+ ValidFileScenarios := ValidFileScenarios.Hashtable();
+ foreach Scenario in Enum::"File Scenario".Ordinals() do
+ ValidFileScenarios.Add(Scenario, Scenario);
+
+ // Convert File Scenario-s to File Account Scenario-s so they can be sorted by "Display Name"
+ repeat
+ IsScenarioValid := ValidFileScenarios.Contains(FileScenarios.Scenario.AsInteger());
+
+ // Add entry for every scenario that exists and uses the file account. Skip the default scenario.
+ if (FileScenarios.Scenario <> Enum::"File Scenario"::Default) and IsScenarioValid then begin
+ TempFileAccountScenarios.Scenario := FileScenarios.Scenario.AsInteger();
+ TempFileAccountScenarios."Account Id" := FileScenarios."Account Id";
+ TempFileAccountScenarios.Connector := FileScenarios.Connector;
+ TempFileAccountScenarios."Display Name" := Format(FileScenarios.Scenario);
+
+ TempFileAccountScenarios.Insert();
+ end;
+ until FileScenarios.Next() = 0;
+
+ TempFileAccountScenarios.SetCurrentKey("Display Name"); // sort scenarios by "Display Name"
+ end;
+
+ local procedure AddEntry(var TempFileAccountScenario: Record "File Account Scenario" temporary; EntryType: Enum "File Account Entry Type"; Scenario: Integer; AccountId: Guid; FileSystemConnector: Enum "Ext. File Storage Connector"; DisplayName: Text[2048]; Default: Boolean; var Position: Integer)
+ begin
+ // Add entry to the File Account Scenario while maintaining the position so that the tree represents the data correctly
+ TempFileAccountScenario.Init();
+ TempFileAccountScenario.EntryType := EntryType;
+ TempFileAccountScenario.Scenario := Scenario;
+ TempFileAccountScenario."Account Id" := AccountId;
+ TempFileAccountScenario.Connector := FileSystemConnector;
+ TempFileAccountScenario."Display Name" := DisplayName;
+ TempFileAccountScenario.Default := Default;
+ TempFileAccountScenario.Position := Position;
+ TempFileAccountScenario.Insert();
+
+ Position := Position + 1;
+ end;
+
+ procedure AddScenarios(TempFileAccountScenario: Record "File Account Scenario" temporary): Boolean
+ var
+ TempSelectedFileAccScenarios: Record "File Account Scenario" temporary;
+ FileScenario: Record "File Scenario";
+ FileScenariosForAccount: Page "File Scenarios for Account";
+ begin
+ FileAccountImpl.CheckPermissions();
+
+ if TempFileAccountScenario.EntryType <> TempFileAccountScenario.EntryType::Account then // wrong entry, the entry should be of type "Account"
+ exit;
+
+ FileScenariosForAccount.Caption := StrSubstNo(ScenariosForAccountCaptionTxt, TempFileAccountScenario."Display Name");
+ FileScenariosForAccount.LookupMode(true);
+ FileScenariosForAccount.SetRecord(TempFileAccountScenario);
+
+ if FileScenariosForAccount.RunModal() <> Action::LookupOK then
+ exit;
+
+ FileScenariosForAccount.GetSelectedScenarios(TempSelectedFileAccScenarios);
+
+ if not TempSelectedFileAccScenarios.FindSet() then
+ exit;
+
+ repeat
+ if not FileScenario.Get(TempSelectedFileAccScenarios.Scenario) then begin
+ FileScenario."Account Id" := TempFileAccountScenario."Account Id";
+ FileScenario.Connector := TempFileAccountScenario.Connector;
+ FileScenario.Scenario := Enum::"File Scenario".FromInteger(TempSelectedFileAccScenarios.Scenario);
+
+ FileScenario.Insert();
+ end else begin
+ FileScenario."Account Id" := TempFileAccountScenario."Account Id";
+ FileScenario.Connector := TempFileAccountScenario.Connector;
+
+ FileScenario.Modify();
+ end;
+ until TempSelectedFileAccScenarios.Next() = 0;
+
+ exit(true);
+ end;
+
+ procedure GetAvailableScenariosForAccount(TempFileAccountScenario: Record "File Account Scenario" temporary; var TempFileAccountScenarios: Record "File Account Scenario" temporary)
+ var
+ Scenario: Record "File Scenario";
+ FileScenario: Codeunit "File Scenario";
+ CurrentScenario, i : Integer;
+ IsAvailable: Boolean;
+ begin
+ TempFileAccountScenarios.Reset();
+ TempFileAccountScenarios.DeleteAll();
+ i := 1;
+
+ foreach CurrentScenario in Enum::"File Scenario".Ordinals() do begin
+ Clear(Scenario);
+ Scenario.SetRange("Account Id", TempFileAccountScenarios."Account Id");
+ Scenario.SetRange(Connector, TempFileAccountScenarios.Connector);
+ Scenario.SetRange(Scenario, CurrentScenario);
+
+ // If the scenario isn't already connected to the file account, then it's available. Natually, we skip the default scenario
+ IsAvailable := Scenario.IsEmpty() and (not (CurrentScenario = Enum::"File Scenario"::Default.AsInteger()));
+
+ // If the scenario is available, allow partner to determine if it should be shown
+ if IsAvailable then
+ FileScenario.OnBeforeInsertAvailableFileScenario(Enum::"File Scenario".FromInteger(CurrentScenario), IsAvailable);
+
+ if IsAvailable then begin
+ TempFileAccountScenarios."Account Id" := TempFileAccountScenarios."Account Id";
+ TempFileAccountScenarios.Connector := TempFileAccountScenarios.Connector;
+ TempFileAccountScenarios.Scenario := CurrentScenario;
+ TempFileAccountScenarios."Display Name" := Format(Enum::"File Scenario".FromInteger(Enum::"File Scenario".Ordinals().Get(i)));
+
+ TempFileAccountScenarios.Insert();
+ end;
+
+ i += 1;
+ end;
+ end;
+
+ procedure ChangeAccount(var TempFileAccountScenario: Record "File Account Scenario" temporary): Boolean
+ var
+ TempSelectedFileAccount: Record "File Account" temporary;
+ FileScenario: Record "File Scenario";
+ FileAccount: Codeunit "File Account";
+ AccountsPage: Page "File Accounts";
+ begin
+ FileAccountImpl.CheckPermissions();
+
+ if not TempFileAccountScenario.FindSet() then
+ exit;
+
+ FileAccount.GetAllAccounts(false, TempSelectedFileAccount);
+ if TempSelectedFileAccount.Get(TempFileAccountScenario."Account Id", TempFileAccountScenario.Connector) then;
+
+ AccountsPage.EnableLookupMode();
+ AccountsPage.SetRecord(TempSelectedFileAccount);
+ AccountsPage.Caption := ChangeFileAccountForScenarioTxt;
+
+ if AccountsPage.RunModal() <> Action::LookupOK then
+ exit;
+
+ AccountsPage.GetAccount(TempSelectedFileAccount);
+
+ if IsNullGuid(TempSelectedFileAccount."Account Id") then // defensive check, no account was selected
+ exit;
+
+ repeat
+ if FileScenario.Get(TempFileAccountScenario.Scenario) then begin
+ FileScenario."Account Id" := TempSelectedFileAccount."Account Id";
+ FileScenario.Connector := TempSelectedFileAccount.Connector;
+
+ FileScenario.Modify();
+ end;
+ until TempFileAccountScenario.Next() = 0;
+
+ exit(true);
+ end;
+
+ procedure DeleteScenario(var TempFileAccountScenario: Record "File Account Scenario" temporary): Boolean
+ var
+ FileScenario: Record "File Scenario";
+ begin
+ FileAccountImpl.CheckPermissions();
+
+ if not TempFileAccountScenario.FindSet() then
+ exit;
+
+ repeat
+ if TempFileAccountScenario.EntryType = TempFileAccountScenario.EntryType::Scenario then begin
+ FileScenario.SetRange(Scenario, TempFileAccountScenario.Scenario);
+ FileScenario.SetRange("Account Id", TempFileAccountScenario."Account Id");
+ FileScenario.SetRange(Connector, TempFileAccountScenario.Connector);
+
+ FileScenario.DeleteAll();
+ end;
+ until TempFileAccountScenario.Next() = 0;
+
+ exit(true);
+ end;
+
+ local procedure GetDefaultAccount(var TempFileAccount: Record "File Account" temporary)
+ var
+ FileScenario: Record "File Scenario";
+ begin
+ if not FileScenario.Get(Enum::"File Scenario"::Default) then
+ exit;
+
+ TempFileAccount."Account Id" := FileScenario."Account Id";
+ TempFileAccount.Connector := FileScenario.Connector;
+ end;
+
+ var
+ FileAccountImpl: Codeunit "File Account Impl.";
+ ChangeFileAccountForScenarioTxt: Label 'Change file account used for the selected scenarios';
+ ScenariosForAccountCaptionTxt: Label 'Assign scenarios to account %1', Comment = '%1 = the name of the e-file account';
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
new file mode 100644
index 0000000000..6470b1f0d0
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenarioSetup.Page.al
@@ -0,0 +1,187 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+using System.Telemetry;
+
+///
+/// Page is used to display file scenarios usage by file accounts.
+///
+page 9452 "File Scenario Setup"
+{
+ Caption = 'File Scenario Assignment';
+ PageType = List;
+ UsageCategory = Administration;
+ ApplicationArea = All;
+ Extensible = false;
+ Editable = false;
+ DeleteAllowed = false;
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ SourceTable = "File Account Scenario";
+ InstructionalText = 'Assign file scenarios';
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(ScenariosByFile)
+ {
+ IndentationColumn = Indentation;
+ IndentationControls = Name;
+ ShowAsTree = true;
+
+ field(Name; Rec."Display Name")
+ {
+ Caption = 'Scenarios by file accounts';
+ ToolTip = 'Specifies the scenarios that are using the file account.';
+ Editable = false;
+ StyleExpr = Style;
+ }
+ field(Default; DefaultTxt)
+ {
+ Caption = 'Default';
+ ToolTip = 'Specifies whether this is the default account to use for scenarios when no other account is specified.';
+ StyleExpr = Style;
+ }
+ }
+ }
+ }
+
+ actions
+ {
+ area(Processing)
+ {
+ group(Account)
+ {
+ action(AddScenario)
+ {
+ Visible = (TypeOfEntry = TypeOfEntry::Account) and CanUserManageFileSetup;
+ Caption = 'Assign scenarios';
+ ToolTip = 'Assign file scenarios for the selected file account. When assigned, everyone will use the account for the scenario. For example, if you assign the Sales Order scenario, everyone will use the account to send sales orders.';
+ Image = NewDocument;
+ Scope = Repeater;
+
+ trigger OnAction()
+ begin
+ TempSelectedFileAccountScenario := Rec;
+ FileScenarioImpl.AddScenarios(Rec);
+
+ FileScenarioImpl.GetScenariosByFileAccount(Rec);
+ SetSelectedRecord();
+ end;
+ }
+ }
+
+ group(Scenario)
+ {
+ action(ChangeAccount)
+ {
+ Visible = (TypeOfEntry = TypeOfEntry::Scenario) and CanUserManageFileSetup;
+ Caption = 'Reassign';
+ ToolTip = 'Reassign the selected scenarios to another file account.';
+ Image = Change;
+ Scope = Repeater;
+
+ trigger OnAction()
+ begin
+ CurrPage.SetSelectionFilter(Rec);
+ TempSelectedFileAccountScenario := Rec;
+
+ FileScenarioImpl.ChangeAccount(Rec);
+ FileScenarioImpl.GetScenariosByFileAccount(Rec); // refresh the data on the page
+ SetSelectedRecord();
+ end;
+ }
+ action(Unassign)
+ {
+ Visible = (TypeOfEntry = TypeOfEntry::Scenario) and CanUserManageFileSetup;
+ Caption = 'Unassign';
+ ToolTip = 'Unassign the selected scenarios. Afterward, the default file account will be used to send files for the scenarios.';
+ Image = Delete;
+ Scope = Repeater;
+
+ trigger OnAction()
+ begin
+ CurrPage.SetSelectionFilter(Rec);
+ TempSelectedFileAccountScenario := Rec;
+
+ FileScenarioImpl.DeleteScenario(Rec);
+ FileScenarioImpl.GetScenariosByFileAccount(Rec); // refresh the data on the page
+ SetSelectedRecord();
+ end;
+ }
+ }
+ }
+ area(Promoted)
+ {
+ group(Category_Process)
+ {
+ actionref(AddScenario_Promoted; AddScenario) { }
+ actionref(ChangeAccount_Promoted; ChangeAccount) { }
+ actionref(Unassign_Promoted; Unassign) { }
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ var
+ FeatureTelemetry: Codeunit "Feature Telemetry";
+ begin
+ FeatureTelemetry.LogUptake('0000CTN', 'File Access', Enum::"Feature Uptake Status"::Discovered);
+ CanUserManageFileSetup := FileAccountImpl.IsUserFileAdmin();
+ FileScenarioImpl.GetScenariosByFileAccount(Rec);
+
+ // Set selection
+ if not Rec.Get(-1, FileAccountId, FileConnector) then
+ if Rec.FindFirst() then;
+ end;
+
+ trigger OnAfterGetRecord()
+ begin
+ DefaultTxt := '';
+
+ TypeOfEntry := Rec.EntryType;
+
+ if TypeOfEntry = TypeOfEntry::Account then begin
+ Indentation := 0;
+ Style := 'Strong';
+ if Rec.Default then
+ DefaultTxt := '✓'
+ end;
+
+ if TypeOfEntry = TypeOfEntry::Scenario then begin
+ Indentation := 1;
+ Style := 'Standard';
+ end;
+ end;
+
+ // Used to set the focus on a file account
+ internal procedure SetFileAccountId(AccountId: Guid; Connector: Enum "Ext. File Storage Connector")
+ begin
+ FileAccountId := AccountId;
+ FileConnector := Connector;
+ end;
+
+ local procedure SetSelectedRecord()
+ begin
+ if not Rec.Get(TempSelectedFileAccountScenario.Scenario, TempSelectedFileAccountScenario."Account Id", TempSelectedFileAccountScenario.Connector) then
+ Rec.FindFirst();
+ end;
+
+ var
+ TempSelectedFileAccountScenario: Record "File Account Scenario" temporary;
+ FileScenarioImpl: Codeunit "File Scenario Impl.";
+ FileAccountImpl: Codeunit "File Account Impl.";
+ FileAccountId: Guid;
+ FileConnector: Enum "Ext. File Storage Connector";
+ Style, DefaultTxt : Text;
+ TypeOfEntry: Enum "File Account Entry Type";
+ Indentation: Integer;
+ CanUserManageFileSetup: Boolean;
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenariosFactBox.Page.al b/src/System Application/App/External File Storage/src/Scenario/FileScenariosFactBox.Page.al
new file mode 100644
index 0000000000..533492a3a7
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenariosFactBox.Page.al
@@ -0,0 +1,42 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Lists of all scenarios assigned to an account.
+///
+page 9453 "File Scenarios FactBox"
+{
+ PageType = ListPart;
+ ApplicationArea = All;
+ Extensible = false;
+ SourceTable = "File Scenario";
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+ Editable = false;
+ ShowFilter = false;
+ LinksAllowed = false;
+ Permissions = tabledata "File Scenario" = r;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(ScenariosByFile)
+ {
+ field(Name; Format(Rec.Scenario))
+ {
+ ToolTip = 'Specifies the name of the file scenario.';
+ Caption = 'File scenario';
+ Editable = false;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/External File Storage/src/Scenario/FileScenariosForAccount.Page.al b/src/System Application/App/External File Storage/src/Scenario/FileScenariosForAccount.Page.al
new file mode 100644
index 0000000000..34fdaa7b3a
--- /dev/null
+++ b/src/System Application/App/External File Storage/src/Scenario/FileScenariosForAccount.Page.al
@@ -0,0 +1,68 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.ExternalFileStorage;
+
+///
+/// Displays the scenarios that could be linked to a provided file account.
+///
+page 9454 "File Scenarios for Account"
+{
+ PageType = List;
+ ApplicationArea = All;
+ Extensible = false;
+ SourceTable = "File Account Scenario";
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+ Editable = false;
+ ShowFilter = false;
+ LinksAllowed = false;
+ InherentPermissions = X;
+ InherentEntitlements = X;
+ UsageCategory = None;
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(ScenariosByFile)
+ {
+ field(Name; Rec."Display Name")
+ {
+ ToolTip = 'Specifies the name of the file scenario.';
+ Caption = 'File scenario';
+ Editable = false;
+ }
+ }
+ }
+ }
+
+ internal procedure GetSelectedScenarios(var TempResultFileAccountScenario: Record "File Account Scenario" temporary)
+ begin
+ TempResultFileAccountScenario.Reset();
+ TempResultFileAccountScenario.DeleteAll();
+
+ CurrPage.SetSelectionFilter(Rec);
+
+ if not Rec.FindSet() then
+ exit;
+
+ repeat
+ TempResultFileAccountScenario.Copy(Rec);
+ TempResultFileAccountScenario.Insert();
+ until Rec.Next() = 0;
+ end;
+
+ trigger OnOpenPage()
+ begin
+ FileScenarioImpl.GetAvailableScenariosForAccount(Rec, Rec);
+ Rec.SetCurrentKey("Display Name");
+ if Rec.FindFirst() then; // Set the selection to the first record
+ end;
+
+ var
+ FileScenarioImpl: Codeunit "File Scenario Impl.";
+}
\ No newline at end of file
diff --git a/src/System Application/App/Permissions/SystemApplicationEdit.PermissionSet.al b/src/System Application/App/Permissions/SystemApplicationEdit.PermissionSet.al
index d5a22806d0..7b1dabc417 100644
--- a/src/System Application/App/Permissions/SystemApplicationEdit.PermissionSet.al
+++ b/src/System Application/App/Permissions/SystemApplicationEdit.PermissionSet.al
@@ -8,6 +8,7 @@ namespace System.Security.AccessControl;
using System.Visualization;
using System.Privacy;
using System.Email;
+using System.ExternalFileStorage;
using System.Text;
using System.Environment.Configuration;
using System.Globalization;
@@ -23,6 +24,7 @@ permissionset 22 "System Application - Edit"
"Data Classification - Edit",
"Email - Edit",
"Entity Text - Edit",
+ "File Storage - Edit",
"Guided Experience - Edit",
"Language - Edit",
"PageScripting - Rec",
diff --git a/src/System Application/App/Permissions/SystemApplicationObjects.PermissionSet.al b/src/System Application/App/Permissions/SystemApplicationObjects.PermissionSet.al
index ef1125095a..061c0af3a6 100644
--- a/src/System Application/App/Permissions/SystemApplicationObjects.PermissionSet.al
+++ b/src/System Application/App/Permissions/SystemApplicationObjects.PermissionSet.al
@@ -14,6 +14,7 @@ using System.Privacy;
using System.Reflection;
using System.Integration;
using System.Integration.Excel;
+using System.ExternalFileStorage;
using System.Email;
using System.Text;
using System.Globalization;
@@ -42,6 +43,7 @@ permissionset 219 "System Application - Objects"
"Entity Text - Objects",
"Extension Management - Objects",
"Feature Key - Objects",
+ "File Storage - Objects",
"Guided Experience - Objects",
"Language - Objects",
"Page Summary Provider - Obj.",
diff --git a/src/System Application/App/Permissions/SystemApplicationRead.PermissionSet.al b/src/System Application/App/Permissions/SystemApplicationRead.PermissionSet.al
index c68f8eea4a..fad19c54e4 100644
--- a/src/System Application/App/Permissions/SystemApplicationRead.PermissionSet.al
+++ b/src/System Application/App/Permissions/SystemApplicationRead.PermissionSet.al
@@ -11,6 +11,7 @@ using System.Visualization;
using System.Privacy;
using System.Environment.Configuration;
using System.Integration.Excel;
+using System.ExternalFileStorage;
using System.Reflection;
using System.Globalization;
using System.Integration;
@@ -39,6 +40,7 @@ permissionset 21 "System Application - Read"
"Extension Management - Read",
"Feature Key - Read",
"Field Selection - Read",
+ "File Storage - Read",
"Guided Experience - Read",
"Headlines - Read",
"Object Selection - Read",
diff --git a/src/System Application/Test Library/External File Storage/Permissions/FileStorageAdmin.PermissionSet.al b/src/System Application/Test Library/External File Storage/Permissions/FileStorageAdmin.PermissionSet.al
new file mode 100644
index 0000000000..98725295fa
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/Permissions/FileStorageAdmin.PermissionSet.al
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+
+permissionset 135810 "File Storage Admin"
+{
+ Assignable = true;
+ IncludedPermissionSets = "File Storage - Admin";
+
+ // Include Test Tables
+ Permissions =
+ tabledata "Test File Connector Setup" = RIMD,
+ tabledata "Test File Account" = RIMD;
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/Permissions/FileStorageEdit.PermissionSet.al b/src/System Application/Test Library/External File Storage/Permissions/FileStorageEdit.PermissionSet.al
new file mode 100644
index 0000000000..414957f7c3
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/Permissions/FileStorageEdit.PermissionSet.al
@@ -0,0 +1,21 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+using System.Environment;
+
+permissionset 135811 "File Storage Edit"
+{
+ Assignable = true;
+ IncludedPermissionSets = "File Storage - Edit";
+
+ // Include Test Tables
+ Permissions =
+ tabledata "Test File Connector Setup" = RIMD,
+ tabledata "Test File Account" = RIMD, // Needed for the Record to get passed in Library Assert
+ tabledata "Scheduled Task" = rd; // Needed for enqueue tests
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/app.json b/src/System Application/Test Library/External File Storage/app.json
new file mode 100644
index 0000000000..973fdf45ff
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/app.json
@@ -0,0 +1,39 @@
+{
+ "id": "f188754b-3ffb-443a-9507-f5fbdae3af2c",
+ "name": "External File Storage Test Library",
+ "publisher": "Microsoft",
+ "brief": "Test library for the External File Storage module",
+ "description": "Test library for the External File Storage module",
+ "version": "26.0.0.0",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
+ "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
+ "help": "https://go.microsoft.com/fwlink/?linkid=2103698",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "",
+ "dependencies": [
+ {
+ "id": "c9c54414-80c3-4cc9-98c6-589158882774",
+ "name": "External File Storage",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b",
+ "name": "Any",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ }
+ ],
+ "screenshots": [
+
+ ],
+ "platform": "26.0.0.0",
+ "idRanges": [
+ {
+ "from": 135810,
+ "to": 135819
+ }
+ ],
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
+ "target": "OnPrem"
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/ExtFileStorageTestLib.Codeunit.al b/src/System Application/Test Library/External File Storage/src/ExtFileStorageTestLib.Codeunit.al
new file mode 100644
index 0000000000..30c3623f30
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/ExtFileStorageTestLib.Codeunit.al
@@ -0,0 +1,25 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+
+codeunit 135813 "Ext. File Storage Test Lib."
+{
+ Permissions = tabledata "File Scenario" = rid;
+
+ procedure GetFileScenarioAccountIdAndFileConnector(Scenario: Enum "File Scenario"; var AccountId: Guid; var ExternalFileStorageConnector: Interface "External File Storage Connector"): Boolean
+ var
+ FileScenarios: Record "File Scenario";
+ begin
+ if not FileScenarios.Get(Scenario) then
+ exit;
+
+ AccountId := FileScenarios."Account Id";
+ ExternalFileStorageConnector := FileScenarios.Connector;
+ exit(true);
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/Mock/ExtFileStorageAccSelMock.Codeunit.al b/src/System Application/Test Library/External File Storage/src/Mock/ExtFileStorageAccSelMock.Codeunit.al
new file mode 100644
index 0000000000..d58c3cce79
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/Mock/ExtFileStorageAccSelMock.Codeunit.al
@@ -0,0 +1,44 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Test.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+
+///
+/// Used to mock selected file accounts on File Accounts page.
+///
+codeunit 135812 "Ext. File Storage Acc Sel Mock"
+{
+ EventSubscriberInstance = Manual;
+
+ var
+ SelectionFilterLbl: Label '%1|%2', Locked = true;
+
+ procedure SelectAccount(AccountId: Guid)
+ begin
+ SelectedAccounts.Add(AccountId);
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"File Account Impl.", 'OnAfterSetSelectionFilter', '', false, false)]
+ local procedure SelectAccounts(var TempFileAccount: Record "File Account" temporary)
+ var
+ AccountId: Guid;
+ SelectionFilter: Text;
+ begin
+ TempFileAccount.Reset();
+
+ foreach AccountId in SelectedAccounts do
+ SelectionFilter := StrSubstNo(SelectionFilterLbl, SelectionFilter, AccountId);
+
+ SelectionFilter := DelChr(SelectionFilter, '<>', '|'); // remove trailing and leading pipes
+
+ if SelectionFilter <> '' then
+ TempFileAccount.SetFilter("Account Id", SelectionFilter);
+ end;
+
+ var
+ SelectedAccounts: List of [Guid];
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/Mock/FileConnectorMock.Codeunit.al b/src/System Application/Test Library/External File Storage/src/Mock/FileConnectorMock.Codeunit.al
new file mode 100644
index 0000000000..9a9b580e0c
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/Mock/FileConnectorMock.Codeunit.al
@@ -0,0 +1,119 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+using System.TestLibraries.Utilities;
+
+codeunit 135810 "File Connector Mock"
+{
+ var
+ Any: Codeunit Any;
+
+ procedure Initialize()
+ var
+ TestFileAccount: Record "Test File Account";
+ TestFileConnectorSetup: Record "Test File Connector Setup";
+ begin
+ TestFileConnectorSetup.DeleteAll();
+ TestFileConnectorSetup.Init();
+ TestFileConnectorSetup.Id := Any.GuidValue();
+ TestFileConnectorSetup."Fail On Send" := false;
+ TestFileConnectorSetup."Fail On Register Account" := false;
+ TestFileConnectorSetup."Unsuccessful Register" := false;
+ TestFileConnectorSetup.Insert();
+
+ TestFileAccount.DeleteAll();
+ end;
+
+ procedure GetAccounts(var FileAccount: Record "File Account")
+ var
+ TestFileAccount: Record "Test File Account";
+ begin
+ if TestFileAccount.FindSet() then
+ repeat
+ FileAccount.Init();
+ FileAccount."Account Id" := TestFileAccount.Id;
+ FileAccount.Name := TestFileAccount.Name;
+ FileAccount.Insert();
+ until TestFileAccount.Next() = 0;
+ end;
+
+ procedure AddAccount(var FileAccount: Record "File Account")
+ var
+ TestFileAccount: Record "Test File Account";
+ begin
+ TestFileAccount.Id := Any.GuidValue();
+ TestFileAccount.Name := CopyStr(Any.AlphanumericText(250), 1, 250);
+ TestFileAccount.Insert();
+
+ FileAccount."Account Id" := TestFileAccount.Id;
+ FileAccount.Name := TestFileAccount.Name;
+ FileAccount.Connector := Enum::"Ext. File Storage Connector"::"Test File Storage Connector";
+ end;
+
+ procedure AddAccount(var Id: Guid)
+ var
+ TestFileAccount: Record "Test File Account";
+ begin
+ TestFileAccount.Id := Any.GuidValue();
+ TestFileAccount.Name := CopyStr(Any.AlphanumericText(250), 1, 250);
+ TestFileAccount.Insert();
+
+ Id := TestFileAccount.Id;
+ end;
+
+ procedure FailOnSend(): Boolean
+ var
+ TestFileConnectorSetup: Record "Test File Connector Setup";
+ begin
+ TestFileConnectorSetup.FindFirst();
+ exit(TestFileConnectorSetup."Fail On Send");
+ end;
+
+ procedure FailOnSend(Fail: Boolean)
+ var
+ TestFileConnectorSetup: Record "Test File Connector Setup";
+ begin
+ TestFileConnectorSetup.FindFirst();
+ TestFileConnectorSetup."Fail On Send" := Fail;
+ TestFileConnectorSetup.Modify();
+ end;
+
+ procedure FailOnRegisterAccount(): Boolean
+ var
+ TestFileConnectorSetup: Record "Test File Connector Setup";
+ begin
+ TestFileConnectorSetup.FindFirst();
+ exit(TestFileConnectorSetup."Fail On Register Account");
+ end;
+
+ procedure FailOnRegisterAccount(Fail: Boolean)
+ var
+ TestFileConnectorSetup: Record "Test File Connector Setup";
+ begin
+ TestFileConnectorSetup.FindFirst();
+ TestFileConnectorSetup."Fail On Register Account" := Fail;
+ TestFileConnectorSetup.Modify();
+ end;
+
+ procedure UnsuccessfulRegister(): Boolean
+ var
+ TestFileConnectorSetup: Record "Test File Connector Setup";
+ begin
+ TestFileConnectorSetup.FindFirst();
+ exit(TestFileConnectorSetup."Unsuccessful Register");
+ end;
+
+ procedure UnsuccessfulRegister(Fail: Boolean)
+ var
+ TestFileConnectorSetup: Record "Test File Connector Setup";
+ begin
+ TestFileConnectorSetup.FindFirst();
+ TestFileConnectorSetup."Unsuccessful Register" := Fail;
+ TestFileConnectorSetup.Modify();
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/Mock/FileScenarioMock.Codeunit.al b/src/System Application/Test Library/External File Storage/src/Mock/FileScenarioMock.Codeunit.al
new file mode 100644
index 0000000000..4952218597
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/Mock/FileScenarioMock.Codeunit.al
@@ -0,0 +1,31 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+
+codeunit 135811 "File Scenario Mock"
+{
+ Permissions = tabledata "File Scenario" = rid;
+
+ procedure AddMapping(FileScenario: Enum "File Scenario"; AccountId: Guid; Connector: Enum "Ext. File Storage Connector")
+ var
+ Scenario: Record "File Scenario";
+ begin
+ Scenario.Scenario := FileScenario;
+ Scenario."Account Id" := AccountId;
+ Scenario.Connector := Connector;
+
+ Scenario.Insert();
+ end;
+
+ procedure DeleteAllMappings()
+ var
+ Scenario: Record "File Scenario";
+ begin
+ Scenario.DeleteAll();
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/TestFileAccount.Table.al b/src/System Application/Test Library/External File Storage/src/TestFileAccount.Table.al
new file mode 100644
index 0000000000..ae06819ff0
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/TestFileAccount.Table.al
@@ -0,0 +1,32 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+table 135810 "Test File Account"
+{
+ DataClassification = SystemMetadata;
+ ReplicateData = false;
+
+ fields
+ {
+ field(1; Id; Guid)
+ {
+ Caption = 'Primary Key';
+ }
+ field(2; Name; Text[250])
+ {
+ Caption = 'Name';
+ }
+ }
+
+ keys
+ {
+ key(PK; Id)
+ {
+ Clustered = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/TestFileConnectorSetup.Table.al b/src/System Application/Test Library/External File Storage/src/TestFileConnectorSetup.Table.al
new file mode 100644
index 0000000000..5c5ca4fb6d
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/TestFileConnectorSetup.Table.al
@@ -0,0 +1,40 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+table 135811 "Test File Connector Setup"
+{
+ DataClassification = SystemMetadata;
+ ReplicateData = false;
+
+ fields
+ {
+ field(1; Id; Guid)
+ {
+ Caption = 'Primary Key';
+ }
+ field(2; "Fail On Send"; Boolean)
+ {
+ Caption = 'Fail On Send';
+ }
+ field(3; "Fail On Register Account"; Boolean)
+ {
+ Caption = 'Fail On Register Account';
+ }
+ field(4; "Unsuccessful Register"; Boolean)
+ {
+ Caption = 'Unsuccessful Register';
+ }
+ }
+
+ keys
+ {
+ key(PK; Id)
+ {
+ Clustered = true;
+ }
+ }
+}
diff --git a/src/System Application/Test Library/External File Storage/src/TestFileScenario.EnumExt.al b/src/System Application/Test Library/External File Storage/src/TestFileScenario.EnumExt.al
new file mode 100644
index 0000000000..dbaf6dc469
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/TestFileScenario.EnumExt.al
@@ -0,0 +1,15 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+
+enumextension 135810 "Test File Scenario" extends "File Scenario"
+{
+ value(135810; "Test File Scenario")
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/TestFileStorageConnector.Codeunit.al b/src/System Application/Test Library/External File Storage/src/TestFileStorageConnector.Codeunit.al
new file mode 100644
index 0000000000..dd52da94dc
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/TestFileStorageConnector.Codeunit.al
@@ -0,0 +1,110 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+
+codeunit 135814 "Test File Storage Connector" implements "External File Storage Connector"
+{
+ procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary);
+ begin
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Type := TempFileAccountContent.Type::Directory;
+ TempFileAccountContent.Name := 'Test Folder';
+ TempFileAccountContent.Insert();
+
+ TempFileAccountContent.Init();
+ TempFileAccountContent.Type := TempFileAccountContent.Type::File;
+ TempFileAccountContent.Name := 'Test.pdf';
+ TempFileAccountContent.Insert();
+ end;
+
+ procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream);
+ begin
+ end;
+
+ procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream);
+ begin
+ end;
+
+ procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text);
+ begin
+ end;
+
+ procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text);
+ begin
+ end;
+
+ procedure FileExists(AccountId: Guid; Path: Text): Boolean;
+ begin
+ end;
+
+ procedure DeleteFile(AccountId: Guid; Path: Text);
+ begin
+ end;
+
+ procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary);
+ begin
+ end;
+
+ procedure CreateDirectory(AccountId: Guid; Path: Text);
+ begin
+ end;
+
+ procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean;
+ begin
+ end;
+
+ procedure DeleteDirectory(AccountId: Guid; Path: Text);
+ begin
+ end;
+
+ procedure GetAccounts(var TempAccounts: Record "File Account" temporary)
+ begin
+ FileConnectorMock.GetAccounts(TempAccounts);
+ end;
+
+ procedure ShowAccountInformation(AccountId: Guid)
+ begin
+ Message('Showing information for account: %1', AccountId);
+ end;
+
+ procedure RegisterAccount(var TempFileAccount: Record "File Account" temporary): Boolean
+ var
+ begin
+ if FileConnectorMock.FailOnRegisterAccount() then
+ Error('Failed to register account');
+
+ if FileConnectorMock.UnsuccessfulRegister() then
+ exit(false);
+
+ TempFileAccount."Account Id" := CreateGuid();
+ TempFileAccount.Name := 'Test account';
+
+ exit(true);
+ end;
+
+ procedure DeleteAccount(AccountId: Guid): Boolean
+ var
+ TestFileAccount: Record "Test File Account";
+ begin
+ if TestFileAccount.Get(AccountId) then
+ exit(TestFileAccount.Delete());
+ exit(false);
+ end;
+
+ procedure GetLogoAsBase64(): Text
+ begin
+ end;
+
+ procedure GetDescription(): Text[250]
+ begin
+ exit('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ornare ante a est commodo interdum. Pellentesque eu diam maximus, faucibus neque ut, viverra leo. Praesent ullamcorper nibh ut pretium dapibus. Nullam eu dui libero. Etiam ac cursus metus.')
+ end;
+
+ var
+ FileConnectorMock: Codeunit "File Connector Mock";
+}
\ No newline at end of file
diff --git a/src/System Application/Test Library/External File Storage/src/TestFileStorageConnector.EnumExt.al b/src/System Application/Test Library/External File Storage/src/TestFileStorageConnector.EnumExt.al
new file mode 100644
index 0000000000..b3ef8af6e7
--- /dev/null
+++ b/src/System Application/Test Library/External File Storage/src/TestFileStorageConnector.EnumExt.al
@@ -0,0 +1,16 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.TestLibraries.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+
+enumextension 135815 "Test File Storage Connector" extends "Ext. File Storage Connector"
+{
+ value(135810; "Test File Storage Connector")
+ {
+ Implementation = "External File Storage Connector" = "Test File Storage Connector";
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/Test/External File Storage/app.json b/src/System Application/Test/External File Storage/app.json
new file mode 100644
index 0000000000..5169bd7d05
--- /dev/null
+++ b/src/System Application/Test/External File Storage/app.json
@@ -0,0 +1,75 @@
+{
+ "id": "fff6eee5-083f-4f07-9a49-e34f7e2aad77",
+ "name": "External File Storage Test",
+ "publisher": "Microsoft",
+ "brief": "Tests for the External File Storage module",
+ "description": "Tests for the External File Storage module",
+ "version": "26.0.0.0",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009",
+ "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
+ "help": "https://go.microsoft.com/fwlink/?linkid=2103698",
+ "url": "https://go.microsoft.com/fwlink/?linkid=724011",
+ "logo": "",
+ "dependencies": [
+ {
+ "id": "c9c54414-80c3-4cc9-98c6-589158882774",
+ "name": "External File Storage",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "f188754b-3ffb-443a-9507-f5fbdae3af2c",
+ "name": "External File Storage Test Library",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "5095f467-0a01-4b99-99d1-9ff1237d286f",
+ "name": "Library Variable Storage",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "e31ad830-3d46-472e-afeb-1d3d35247943",
+ "name": "BLOB Storage",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "0846d207-5dec-4c1b-afd8-6a25e1e14b9d",
+ "name": "Base64 Convert",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14",
+ "name": "Library Assert",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b",
+ "name": "Any",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ },
+ {
+ "id": "40860557-a18d-42ad-aecb-22b7dd80dc80",
+ "name": "Permissions Mock",
+ "publisher": "Microsoft",
+ "version": "26.0.0.0"
+ }
+ ],
+ "screenshots": [
+
+ ],
+ "platform": "26.0.0.0",
+ "idRanges": [
+ {
+ "from": 134750,
+ "to": 134754
+ }
+ ],
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520",
+ "target": "OnPrem"
+}
\ No newline at end of file
diff --git a/src/System Application/Test/External File Storage/src/FileAccountsTest.Codeunit.al b/src/System Application/Test/External File Storage/src/FileAccountsTest.Codeunit.al
new file mode 100644
index 0000000000..900d38ec95
--- /dev/null
+++ b/src/System Application/Test/External File Storage/src/FileAccountsTest.Codeunit.al
@@ -0,0 +1,502 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Test.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+using System.TestLibraries.ExternalFileStorage;
+using System.TestLibraries.Utilities;
+using System.TestLibraries.Security.AccessControl;
+
+codeunit 134750 "File Accounts Test"
+{
+ Subtype = Test;
+ TestPermissions = Disabled;
+
+ var
+ Assert: Codeunit "Library Assert";
+ PermissionsMock: Codeunit "Permissions Mock";
+ AccountToSelect: Guid;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure AccountsAppearOnThePageTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ AccountsPage: TestPage "File Accounts";
+ begin
+ // [Scenario] When there's a File account for a connector, it appears on the accounts page
+
+ // [Given] A File account
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(TempFileAccount);
+
+ PermissionsMock.Set('File Storage Edit');
+
+ // [When] The accounts page is open
+ AccountsPage.OpenView();
+
+ // [Then] The file entry is visible on the page
+ Assert.IsTrue(AccountsPage.GoToKey(TempFileAccount."Account Id", TempFileAccount.Connector), 'The File account should be on the page');
+
+ Assert.AreEqual(TempFileAccount.Name, Format(AccountsPage.NameField), 'The account name on the page is wrong');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure TwoAccountsAppearOnThePageTest()
+ var
+ TempFirstFileAccount, TempSecondFileAccount : Record "File Account" temporary;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ AccountsPage: TestPage "File Accounts";
+ begin
+ // [Scenario] When there's a File account for a connector, it appears on the accounts page
+
+ // [Given] Two File accounts
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(TempFirstFileAccount);
+ FileConnectorMock.AddAccount(TempSecondFileAccount);
+
+ PermissionsMock.Set('File Storage Edit');
+
+ // [When] The accounts page is open
+ AccountsPage.OpenView();
+
+ // [Then] The file entries are visible on the page
+ Assert.IsTrue(AccountsPage.GoToKey(TempFirstFileAccount."Account Id", Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The first File account should be on the page');
+ Assert.AreEqual(TempFirstFileAccount.Name, Format(AccountsPage.NameField), 'The first account name on the page is wrong');
+
+ Assert.IsTrue(AccountsPage.GoToKey(TempSecondFileAccount."Account Id", Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The second File account should be on the page');
+ Assert.AreEqual(TempSecondFileAccount.Name, Format(AccountsPage.NameField), 'The second account name on the page is wrong');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure AddNewAccountTest()
+ var
+ FileConnectorMock: Codeunit "File Connector Mock";
+ AccountWizardPage: TestPage "File Account Wizard";
+ begin
+ // [SCENARIO] A new Account can be added through the Account Wizard
+ PermissionsMock.Set('File Storage Admin');
+
+ FileConnectorMock.Initialize();
+
+ // [WHEN] The AddAccount action is invoked
+ AccountWizardPage.Trap();
+ Page.Run(Page::"File Account Wizard");
+
+ // [WHEN] The next field is invoked
+ AccountWizardPage.Next.Invoke();
+
+ // [THEN] The connector screen is shown and the test connector is shown
+ Assert.IsTrue(AccountWizardPage.Name.Visible(), 'Connector Name should be visible');
+ Assert.IsTrue(AccountWizardPage.Details.Visible(), 'Connector Details should be visible');
+
+ Assert.IsTrue(AccountWizardPage.GoToKey(Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'Test File connector was not shown in the page');
+
+ // [WHEN] The Name field is drilled down
+ AccountWizardPage.Next.Invoke();
+
+ // [THEN] The Connector registers the Account and the last page is shown
+ Assert.AreEqual(AccountWizardPage.NameField.Value(), 'Test account', 'A different name was expected');
+ Assert.AreEqual(AccountWizardPage.DefaultField.AsBoolean(), true, 'Default should be set to true if it''s the first account to be set up');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ [HandlerFunctions('AddAccountModalPageHandler')]
+ procedure AddNewAccountActionRunsPageInModalTest()
+ var
+ AccountsPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] The add Account action open the Account Wizard page in modal mode
+ PermissionsMock.Set('File Storage Admin');
+
+ AccountsPage.OpenView();
+ // [WHEN] The AddAccount action is invoked
+ AccountsPage.AddAccount.Invoke();
+
+ // Verify with AddAccountModalPageHandler
+ end;
+
+ [Test]
+ procedure GetAllAccountsTest()
+ var
+ TempFileAccountBuffer, TempFileAccounts : Record "File Account" temporary;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccount: Codeunit "File Account";
+ begin
+ // [SCENARIO] GetAllAccounts retrieves all the registered accounts
+
+ // [GIVEN] A connector is installed and no account is added
+ FileConnectorMock.Initialize();
+
+ PermissionsMock.Set('File Storage Edit');
+
+ // [WHEN] GetAllAccounts is called
+ FileAccount.GetAllAccounts(TempFileAccounts);
+
+ // [THEN] The returned record is empty (there are no registered accounts)
+ Assert.IsTrue(TempFileAccounts.IsEmpty(), 'Record should be empty');
+
+ // [GIVEN] An account is added to the connector
+ FileConnectorMock.AddAccount(TempFileAccountBuffer);
+
+ // [WHEN] GetAllAccounts is called
+ FileAccount.GetAllAccounts(TempFileAccounts);
+
+ // [THEN] The returned record is not empty and the values are as expected
+ Assert.AreEqual(1, TempFileAccounts.Count(), 'Record should not be empty');
+ TempFileAccounts.FindFirst();
+ Assert.AreEqual(TempFileAccountBuffer."Account Id", TempFileAccounts."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccounts.Connector, 'Wrong connector');
+ Assert.AreEqual(TempFileAccountBuffer.Name, TempFileAccounts.Name, 'Wrong account name');
+ end;
+
+ [Test]
+ procedure IsAnyAccountRegisteredTest()
+ var
+ FileAccount: Codeunit "File Account";
+ FileConnectorMock: Codeunit "File Connector Mock";
+ AccountId: Guid;
+ begin
+ // [SCENARIO] File Account Exists works as expected
+
+ // [GIVEN] A connector is installed and no account is added
+ FileConnectorMock.Initialize();
+
+ PermissionsMock.Set('File Storage Edit');
+
+ // [WHEN] Calling IsAnyAccountRegistered
+ // [THEN] it evaluates to false
+ Assert.IsFalse(FileAccount.IsAnyAccountRegistered(), 'There should be no registered accounts');
+
+ // [WHEN] An File account is added
+ FileConnectorMock.AddAccount(AccountId);
+
+ // [WHEN] Calling IsAnyAccountRegistered
+ // [THEN] it evaluates to true
+ Assert.IsTrue(FileAccount.IsAnyAccountRegistered(), 'There should be a registered account');
+ end;
+
+ [Test]
+ [HandlerFunctions('ConfirmYesHandler')]
+ procedure DeleteAllAccountsTest()
+ var
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccountsSelectionMock: Codeunit "Ext. File Storage Acc Sel Mock";
+ FirstAccountId, SecondAccountId, ThirdAccountId : Guid;
+ FileAccountsTestPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] When all accounts are deleted, the File Accounts page is empty
+ PermissionsMock.Set('File Storage Admin');
+
+ // [GIVEN] A connector is installed and three account are added
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(FirstAccountId);
+ FileConnectorMock.AddAccount(SecondAccountId);
+ FileConnectorMock.AddAccount(ThirdAccountId);
+
+ // [WHEN] Open the File Accounts page
+ FileAccountsTestPage.OpenView();
+
+ // [WHEN] Select all of the accounts
+ BindSubscription(FileAccountsSelectionMock);
+ FileAccountsSelectionMock.SelectAccount(FirstAccountId);
+ FileAccountsSelectionMock.SelectAccount(SecondAccountId);
+ FileAccountsSelectionMock.SelectAccount(ThirdAccountId);
+
+ // [WHEN] Delete action is invoked and the action is confirmed (see ConfirmYesHandler)
+ FileAccountsTestPage.Delete.Invoke();
+
+ // [THEN] The page is empty
+ Assert.IsFalse(FileAccountsTestPage.First(), 'The File Accounts page should be empty');
+ end;
+
+ [Test]
+ [HandlerFunctions('ConfirmNoHandler')]
+ procedure DeleteAllAccountsCancelTest()
+ var
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccountsSelectionMock: Codeunit "Ext. File Storage Acc Sel Mock";
+ FirstAccountId, SecondAccountId, ThirdAccountId : Guid;
+ FileAccountsTestPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] When all accounts are about to be deleted but the action in canceled, the File Accounts page contains all of them.
+ PermissionsMock.Set('File Storage Admin');
+
+ // [GIVEN] A connector is installed and three account are added
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(FirstAccountId);
+ FileConnectorMock.AddAccount(SecondAccountId);
+ FileConnectorMock.AddAccount(ThirdAccountId);
+
+ // [WHEN] Open the File Accounts page
+ FileAccountsTestPage.OpenView();
+
+ // [WHEN] Select all of the accounts
+ BindSubscription(FileAccountsSelectionMock);
+ FileAccountsSelectionMock.SelectAccount(FirstAccountId);
+ FileAccountsSelectionMock.SelectAccount(SecondAccountId);
+ FileAccountsSelectionMock.SelectAccount(ThirdAccountId);
+
+ // [WHEN] Delete action is invoked and the action is not confirmed (see ConfirmNoHandler)
+ FileAccountsTestPage.Delete.Invoke();
+
+ // [THEN] All of the accounts are on the page
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(FirstAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The first File account should be on the page');
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(SecondAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The second File account should be on the page');
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(ThirdAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The third File account should be on the page');
+ end;
+
+ [Test]
+ [HandlerFunctions('ConfirmYesHandler')]
+ procedure DeleteSomeAccountsTest()
+ var
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccountsSelectionMock: Codeunit "Ext. File Storage Acc Sel Mock";
+ FirstAccountId, SecondAccountId, ThirdAccountId : Guid;
+ FileAccountsTestPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] When some accounts are deleted, they cannot be found on the page
+ PermissionsMock.Set('File Storage Admin');
+
+ // [GIVEN] A connector is installed and three account are added
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(FirstAccountId);
+ FileConnectorMock.AddAccount(SecondAccountId);
+ FileConnectorMock.AddAccount(ThirdAccountId);
+
+ // [WHEN] Open the File Accounts page
+ FileAccountsTestPage.OpenView();
+
+ // [WHEN] Select only two of the accounts
+ BindSubscription(FileAccountsSelectionMock);
+ FileAccountsSelectionMock.SelectAccount(FirstAccountId);
+ FileAccountsSelectionMock.SelectAccount(ThirdAccountId);
+
+ // [WHEN] Delete action is invoked and the action is confirmed (see ConfirmYesHandler)
+ FileAccountsTestPage.Delete.Invoke();
+
+ // [THEN] The deleted accounts are not on the page, the non-deleted accounts are on the page.
+ Assert.IsFalse(FileAccountsTestPage.GoToKey(FirstAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The first File account should not be on the page');
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(SecondAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The second File account should be on the page');
+ Assert.IsFalse(FileAccountsTestPage.GoToKey(ThirdAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The third File account should not be on the page');
+ end;
+
+ [Test]
+ [HandlerFunctions('ConfirmYesHandler')]
+ procedure DeleteNonDefaultAccountTest()
+ var
+ TempSecondAccount: Record "File Account" temporary;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccountsSelectionMock: Codeunit "Ext. File Storage Acc Sel Mock";
+ FileScenario: Codeunit "File Scenario";
+ FirstAccountId, ThirdAccountId : Guid;
+ FileAccountsTestPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] When the a non default account is deleted, the user is not prompted to choose a new default account.
+ PermissionsMock.Set('File Storage Admin');
+
+ // [GIVEN] A connector is installed and three account are added
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(FirstAccountId);
+ FileConnectorMock.AddAccount(TempSecondAccount);
+ FileConnectorMock.AddAccount(ThirdAccountId);
+
+ // [GIVEN] The second account is set as default
+ FileScenario.SetDefaultFileAccount(TempSecondAccount);
+
+ // [WHEN] Open the File Accounts page
+ FileAccountsTestPage.OpenView();
+
+ // [WHEN] Select a non-default account
+ BindSubscription(FileAccountsSelectionMock);
+ FileAccountsSelectionMock.SelectAccount(FirstAccountId);
+
+ // [WHEN] Delete action is invoked and the action is confirmed (see ConfirmYesHandler)
+ FileAccountsTestPage.Delete.Invoke();
+
+ // [THEN] The deleted accounts are not on the page, the non-deleted accounts are on the page.
+ Assert.IsFalse(FileAccountsTestPage.GoToKey(FirstAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The first File account should not be on the page');
+
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(TempSecondAccount."Account Id", Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The second File account should be on the page');
+ Assert.IsTrue(GetDefaultFieldValueAsBoolean(FileAccountsTestPage.DefaultField.Value), 'The second account should be marked as default');
+
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(ThirdAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The third File account should be on the page');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileAccountsTestPage.DefaultField.Value), 'The third account should not be marked as default');
+ end;
+
+ [Test]
+ [HandlerFunctions('ConfirmYesHandler')]
+ procedure DeleteDefaultAccountTest()
+ var
+ TempSecondAccount: Record "File Account" temporary;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccountsSelectionMock: Codeunit "Ext. File Storage Acc Sel Mock";
+ FileScenario: Codeunit "File Scenario";
+ FirstAccountId, ThirdAccountId : Guid;
+ FileAccountsTestPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] When the default account is deleted, the user is not prompted to choose a new default account if there's only one account left
+ PermissionsMock.Set('File Storage Admin');
+
+ // [GIVEN] A connector is installed and three account are added
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(FirstAccountId);
+ FileConnectorMock.AddAccount(TempSecondAccount);
+ FileConnectorMock.AddAccount(ThirdAccountId);
+
+ // [GIVEN] The second account is set as default
+ FileScenario.SetDefaultFileAccount(TempSecondAccount);
+
+ // [WHEN] Open the File Accounts page
+ FileAccountsTestPage.OpenView();
+
+ // [WHEN] Select accounts including the default one
+ BindSubscription(FileAccountsSelectionMock);
+ FileAccountsSelectionMock.SelectAccount(TempSecondAccount."Account Id");
+ FileAccountsSelectionMock.SelectAccount(ThirdAccountId);
+
+ // [WHEN] Delete action is invoked and the action is confirmed (see ConfirmYesHandler)
+ FileAccountsTestPage.Delete.Invoke();
+
+ // [THEN] The deleted accounts are not on the page, the non-deleted accounts are on the page.
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(FirstAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The first File account should be on the page');
+ Assert.IsTrue(GetDefaultFieldValueAsBoolean(FileAccountsTestPage.DefaultField.Value), 'The first account should be marked as default');
+
+ Assert.IsFalse(FileAccountsTestPage.GoToKey(TempSecondAccount."Account Id", Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The second File account should not be on the page');
+ Assert.IsFalse(FileAccountsTestPage.GoToKey(ThirdAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The third File account should not be on the page');
+ end;
+
+ [Test]
+ [HandlerFunctions('ConfirmYesHandler,ChooseNewDefaultAccountCancelHandler')]
+ procedure DeleteDefaultAccountPromptNewAccountCancelTest()
+ var
+ TempSecondAccount: Record "File Account" temporary;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccountsSelectionMock: Codeunit "Ext. File Storage Acc Sel Mock";
+ FileScenario: Codeunit "File Scenario";
+ FirstAccountId, ThirdAccountId : Guid;
+ FileAccountsTestPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] When the default account is deleted, the user is prompted to choose a new default account but they cancel.
+ PermissionsMock.Set('File Storage Admin');
+
+ // [GIVEN] A connector is installed and three account are added
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(FirstAccountId);
+ FileConnectorMock.AddAccount(TempSecondAccount);
+ FileConnectorMock.AddAccount(ThirdAccountId);
+
+ // [GIVEN] The second account is set as default
+ FileScenario.SetDefaultFileAccount(TempSecondAccount);
+
+ // [WHEN] Open the File Accounts page
+ FileAccountsTestPage.OpenView();
+
+ // [WHEN] Select the default account
+ BindSubscription(FileAccountsSelectionMock);
+ FileAccountsSelectionMock.SelectAccount(TempSecondAccount."Account Id");
+
+ // [WHEN] Delete action is invoked and the action is confirmed (see ConfirmYesHandler)
+ AccountToSelect := ThirdAccountId; // The third account is selected as the new default account
+ FileAccountsTestPage.Delete.Invoke();
+
+ // [THEN] The default account was deleted and there is no new default account
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(FirstAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The first File account should be on the page');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileAccountsTestPage.DefaultField.Value), 'The third account should not be marked as default');
+
+ Assert.IsFalse(FileAccountsTestPage.GoToKey(TempSecondAccount."Account Id", Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The second File account should not be on the page');
+
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(ThirdAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The third File account should be on the page');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileAccountsTestPage.DefaultField.Value), 'The third account should not be marked as default');
+ end;
+
+ [Test]
+ [HandlerFunctions('ConfirmYesHandler,ChooseNewDefaultAccountHandler')]
+ procedure DeleteDefaultAccountPromptNewAccountTest()
+ var
+ TempSecondAccount: Record "File Account" temporary;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileAccountsSelectionMock: Codeunit "Ext. File Storage Acc Sel Mock";
+ FileScenario: Codeunit "File Scenario";
+ FirstAccountId, ThirdAccountId : Guid;
+ FileAccountsTestPage: TestPage "File Accounts";
+ begin
+ // [SCENARIO] When the default account is deleted, the user is prompted to choose a new default account
+ PermissionsMock.Set('File Storage Admin');
+
+ // [GIVEN] A connector is installed and three account are added
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(FirstAccountId);
+ FileConnectorMock.AddAccount(TempSecondAccount);
+ FileConnectorMock.AddAccount(ThirdAccountId);
+
+ // [GIVEN] The second account is set as default
+ FileScenario.SetDefaultFileAccount(TempSecondAccount);
+
+ // [WHEN] Open the File Accounts page
+ FileAccountsTestPage.OpenView();
+
+ // [WHEN] Select the default account
+ BindSubscription(FileAccountsSelectionMock);
+ FileAccountsSelectionMock.SelectAccount(TempSecondAccount."Account Id");
+
+ // [WHEN] Delete action is invoked and the action is confirmed (see ConfirmYesHandler)
+ AccountToSelect := ThirdAccountId; // The third account is selected as the new default account
+ FileAccountsTestPage.Delete.Invoke();
+
+ // [THEN] The second account is not on the page, the third account is set as default
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(FirstAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The first File account should be on the page');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileAccountsTestPage.DefaultField.Value), 'The first account should not be marked as default');
+
+ Assert.IsFalse(FileAccountsTestPage.GoToKey(TempSecondAccount."Account Id", Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The second File account should not be on the page');
+
+ Assert.IsTrue(FileAccountsTestPage.GoToKey(ThirdAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector"), 'The third File account should be on the page');
+ Assert.IsTrue(GetDefaultFieldValueAsBoolean(FileAccountsTestPage.DefaultField.Value), 'The third account should be marked as default');
+ end;
+
+ [ModalPageHandler]
+ procedure AddAccountModalPageHandler(var AccountWizardTestPage: TestPage "File Account Wizard")
+ begin
+ end;
+
+ [ModalPageHandler]
+ procedure ChooseNewDefaultAccountCancelHandler(var AccountsPage: TestPage "File Accounts")
+ begin
+ AccountsPage.Cancel().Invoke();
+ end;
+
+ [ModalPageHandler]
+ procedure ChooseNewDefaultAccountHandler(var AccountsPage: TestPage "File Accounts")
+ begin
+ AccountsPage.GoToKey(AccountToSelect, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+ AccountsPage.OK().Invoke();
+ end;
+
+ [ConfirmHandler]
+ procedure ConfirmYesHandler(Question: Text[1024]; var Reply: Boolean)
+ begin
+ Reply := true;
+ end;
+
+ [ConfirmHandler]
+ procedure ConfirmNoHandler(Question: Text[1024]; var Reply: Boolean)
+ begin
+ Reply := false;
+ end;
+
+ local procedure GetDefaultFieldValueAsBoolean(DefaultFieldValue: Text): Boolean
+ begin
+ exit(DefaultFieldValue = '✓');
+ end;
+}
diff --git a/src/System Application/Test/External File Storage/src/FileScenarioPageTest.Codeunit.al b/src/System Application/Test/External File Storage/src/FileScenarioPageTest.Codeunit.al
new file mode 100644
index 0000000000..c1fe7dd07e
--- /dev/null
+++ b/src/System Application/Test/External File Storage/src/FileScenarioPageTest.Codeunit.al
@@ -0,0 +1,206 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Test.ExternalFileStorage;
+
+using System.TestLibraries.ExternalFileStorage;
+using System.ExternalFileStorage;
+using System.TestLibraries.Utilities;
+using System.TestLibraries.Security.AccessControl;
+
+codeunit 134751 "File Scenario Page Test"
+{
+ Subtype = Test;
+
+ var
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileScenarioMock: Codeunit "File Scenario Mock";
+ Assert: Codeunit "Library Assert";
+ PermissionsMock: Codeunit "Permissions Mock";
+ DisplayNameTxt: Label '%1', Locked = true;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure PageOpenNoData()
+ var
+ FileScenarioPage: TestPage "File Scenario Setup";
+ begin
+ // [Scenario] The "File Scenario Setup" shows no data when there are no file accounts
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] No file account is registered.
+ FileConnectorMock.Initialize();
+
+ // [When] Opening the the page
+ FileScenarioPage.Trap();
+ FileScenarioPage.OpenView();
+
+ // [Then] There is no data on the page
+ Assert.IsFalse(FileScenarioPage.First(), 'There should be no data on the page');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure PageOpenOneEntryTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ FileScenarioPage: TestPage "File Scenario Setup";
+ begin
+ // [Scenario] The "File Scenario Setup" shows one entry when there is only one file account and no scenarios
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] One file account is registered.
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(TempFileAccount);
+
+ // [When] Opening the the page
+ FileScenarioPage.Trap();
+ FileScenarioPage.OpenView();
+
+ // [Then] There is one entry on the page and it is not set as default
+ Assert.IsTrue(FileScenarioPage.First(), 'There should be an entry on the page');
+
+ // Properties are as expected
+ Assert.AreEqual(StrSubstNo(DisplayNameTxt, TempFileAccount.Name), FileScenarioPage.Name.Value, 'Wrong entry name');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileScenarioPage.Default.Value), 'The account should not be marked as default');
+
+ // Actions visibility is as expected
+ Assert.IsTrue(FileScenarioPage.AddScenario.Visible(), 'The action "Add Scenarios" should be visible');
+ Assert.IsFalse(FileScenarioPage.ChangeAccount.Visible(), 'The action "Change Accounts" should not be visible');
+ Assert.IsFalse(FileScenarioPage.Unassign.Visible(), 'The action "Unassign" should not be visible');
+
+ Assert.IsFalse(FileScenarioPage.Next(), 'There should not be another entry on the page');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure PageOpenOneDefaultEntryTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ FileScenarioPage: TestPage "File Scenario Setup";
+ begin
+ // [Scenario] The "File Scenario Setup" shows one entry when there is only one file account and no scenarios
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] One file account is registered and it's set as default.
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(TempFileAccount);
+
+ FileScenarioMock.DeleteAllMappings();
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, TempFileAccount."Account Id", TempFileAccount.Connector);
+
+ // [When] Opening the the page
+ FileScenarioPage.Trap();
+ FileScenarioPage.OpenView();
+
+ // [Then] There is one entry on the page and it is set as default
+ Assert.IsTrue(FileScenarioPage.First(), 'There should be an entry on the page');
+
+ // Properties are as expected
+ Assert.AreEqual(StrSubstNo(DisplayNameTxt, TempFileAccount.Name), FileScenarioPage.Name.Value, 'Wrong entry name');
+ Assert.IsTrue(GetDefaultFieldValueAsBoolean(FileScenarioPage.Default.Value), 'The account should be marked as default');
+
+ // Actions visibility is as expected
+ Assert.IsTrue(FileScenarioPage.AddScenario.Visible(), 'The action "Add Scenarios" should be visible');
+ Assert.IsFalse(FileScenarioPage.ChangeAccount.Visible(), 'The action "Change Accounts" should not be visible');
+ Assert.IsFalse(FileScenarioPage.Unassign.Visible(), 'The action "Unassign" should not be visible');
+
+ Assert.IsFalse(FileScenarioPage.Next(), 'There should not be another entry on the page');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure PageOpenOneAcountsTwoScenariosTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ FileScenarioPage: TestPage "File Scenario Setup";
+ begin
+ // [Scenario] Having one default account with a non-default scenario assigned displays properly on "File Scenario Setup"
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] One file account is registered and it's set as default.
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(TempFileAccount);
+
+ FileScenarioMock.DeleteAllMappings();
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, TempFileAccount."Account Id", TempFileAccount.Connector);
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::"Test File Scenario", TempFileAccount."Account Id", TempFileAccount.Connector);
+
+ // [When] Opening the the page
+ FileScenarioPage.Trap();
+ FileScenarioPage.OpenView();
+
+ // [Then] There is one entry on the page and it is set as default. There's another entry for the other assigned scenario
+ Assert.IsTrue(FileScenarioPage.First(), 'There should be data on the page');
+
+ // Properties are as expected
+ Assert.AreEqual(StrSubstNo(DisplayNameTxt, TempFileAccount.Name), FileScenarioPage.Name.Value, 'Wrong entry name');
+ Assert.IsTrue(GetDefaultFieldValueAsBoolean(FileScenarioPage.Default.Value), 'The account should be marked as default');
+
+ // Actions visibility is as expected
+ Assert.IsTrue(FileScenarioPage.AddScenario.Visible(), 'The action "Add Scenarios" should be visible');
+ Assert.IsFalse(FileScenarioPage.ChangeAccount.Visible(), 'The action "Change Accounts" should not be visible');
+ Assert.IsFalse(FileScenarioPage.Unassign.Visible(), 'The action "Unassign" should not be visible');
+
+ FileScenarioPage.Expand(true);
+ Assert.IsTrue(FileScenarioPage.Next(), 'There should be another entry on the page');
+
+ // Properties are as expected
+ Assert.AreEqual(Format(Enum::"File Scenario"::"Test File Scenario"), FileScenarioPage.Name.Value, 'Wrong entry name');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileScenarioPage.Default.Value), 'The account should not be marked as default');
+
+ // Actions visibility is as expected
+ Assert.IsFalse(FileScenarioPage.AddScenario.Visible(), 'The action "Add Scenarios" should be visible');
+ Assert.IsTrue(FileScenarioPage.ChangeAccount.Visible(), 'The action "Change Accounts" should not be visible');
+ Assert.IsTrue(FileScenarioPage.Unassign.Visible(), 'The action "Unassign" should not be visible');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ [TransactionModel(TransactionModel::AutoRollback)]
+ procedure PageOpenTwoAcountsTwoScenariosTest()
+ var
+ TempFirstFileAccount, TempSecondFileAccount : Record "File Account" temporary;
+ FileScenarioPage: TestPage "File Scenario Setup";
+ begin
+ // [Scenario] The "File Scenario Setup" shows three entries when there are two accounts - one with the default scenario and one with a non-default scenario
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] Two file accounts are registered. One is set as default.
+ FileConnectorMock.Initialize();
+ FileConnectorMock.AddAccount(TempFirstFileAccount);
+ FileConnectorMock.AddAccount(TempSecondFileAccount);
+
+ FileScenarioMock.DeleteAllMappings();
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, TempFirstFileAccount."Account Id", TempFirstFileAccount.Connector);
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::"Test File Scenario", TempSecondFileAccount."Account Id", TempSecondFileAccount.Connector);
+
+ // [When] Opening the the page
+ FileScenarioPage.Trap();
+ FileScenarioPage.OpenView();
+
+ // [Then] There are three entries on the page. One is set as default
+ Assert.IsTrue(FileScenarioPage.GoToKey(-1, TempFirstFileAccount."Account Id", TempFirstFileAccount.Connector), 'There should be data on the page');
+ Assert.AreEqual(StrSubstNo(DisplayNameTxt, TempFirstFileAccount.Name), FileScenarioPage.Name.Value, 'Wrong first entry name');
+ Assert.IsTrue(GetDefaultFieldValueAsBoolean(FileScenarioPage.Default.Value), 'The account should be marked as default');
+
+ Assert.IsTrue(FileScenarioPage.GoToKey(-1, TempSecondFileAccount."Account Id", TempSecondFileAccount.Connector), 'There should be another entry on the page');
+ Assert.AreEqual(StrSubstNo(DisplayNameTxt, TempSecondFileAccount.Name), FileScenarioPage.Name.Value, 'Wrong second entry name');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileScenarioPage.Default.Value), 'The account should not be marked as default');
+
+ FileScenarioPage.Expand(true);
+ Assert.IsTrue(FileScenarioPage.Next(), 'There should be a third entry on the page');
+ Assert.AreEqual(Format(Enum::"File Scenario"::"Test File Scenario"), FileScenarioPage.Name.Value, 'Wrong third entry name');
+ Assert.IsFalse(GetDefaultFieldValueAsBoolean(FileScenarioPage.Default.Value), 'The account should not be marked as default');
+ end;
+
+ local procedure GetDefaultFieldValueAsBoolean(DefaultFieldValue: Text): Boolean
+ begin
+ exit(DefaultFieldValue = '✓');
+ end;
+}
\ No newline at end of file
diff --git a/src/System Application/Test/External File Storage/src/FileScenarioTest.Codeunit.al b/src/System Application/Test/External File Storage/src/FileScenarioTest.Codeunit.al
new file mode 100644
index 0000000000..d989b615eb
--- /dev/null
+++ b/src/System Application/Test/External File Storage/src/FileScenarioTest.Codeunit.al
@@ -0,0 +1,283 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace System.Test.ExternalFileStorage;
+
+using System.ExternalFileStorage;
+using System.TestLibraries.ExternalFileStorage;
+using System.TestLibraries.Utilities;
+using System.TestLibraries.Security.AccessControl;
+
+codeunit 134752 "File Scenario Test"
+{
+ Subtype = Test;
+
+ var
+ Any: Codeunit Any;
+ FileConnectorMock: Codeunit "File Connector Mock";
+ FileScenario: Codeunit "File Scenario";
+ FileScenarioMock: Codeunit "File Scenario Mock";
+ Assert: Codeunit "Library Assert";
+ PermissionsMock: Codeunit "Permissions Mock";
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountScenarioNotExistsTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ begin
+ // [Scenario] When the File scenario isn't mapped an File account, GetFileAccount returns false
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] No mappings between Files and scenarios
+ Initialize();
+
+ // [When] calling GetFileAccount
+ // [Then] false is returned
+ Assert.IsFalse(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should not be any account');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountNotExistsTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ NonExistentAccountId: Guid;
+ begin
+ // [Scenario] When the File scenario is mapped non-existing File account, GetFileAccount returns false
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] An File scenario pointing to a non-existing File account
+ Initialize();
+ NonExistentAccountId := Any.GuidValue();
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::"Test File Scenario", NonExistentAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+
+ // [When] calling GetFileAccount
+ // [Then] false is returned
+ Assert.IsFalse(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should not be any account mapped to the scenario');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountDefaultNotExistsTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ NonExistentAccountId: Guid;
+ begin
+ // [Scenario] When the default File scenario is mapped to a non-existing File account, GetFileAccount returns false
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] An File scenario isn't mapped to a account and the default scenario is mapped to a non-existing account
+ Initialize();
+ NonExistentAccountId := Any.GuidValue();
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, NonExistentAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+
+ // [When] calling GetFileAccount
+ // [Then] false is returned
+ Assert.IsFalse(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should not be any account mapped to the scenario');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountDefaultExistsTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ AccountId: Guid;
+ begin
+ // [Scenario] When the default File scenario is mapped to an existing File account, GetFileAccount returns that account
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] An File scenario isn't mapped to an account and the default scenario is mapped to an existing account
+ Initialize();
+ FileConnectorMock.AddAccount(AccountId);
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, AccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+
+ // [When] calling GetFileAccount
+ // [Then] true is returned and the File account is as expected
+ Assert.IsTrue(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should be an File account');
+ Assert.AreEqual(AccountId, TempFileAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong connector');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountExistsTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ AccountId: Guid;
+ begin
+ // [Scenario] When the File scenario is mapped to an existing File account, GetFileAccount returns that account
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] An File scenario is mapped to an account
+ Initialize();
+ FileConnectorMock.AddAccount(AccountId);
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::"Test File Scenario", AccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+
+ // [When] calling GetFileAccount
+ // [Then] true is returned and the File account is as expected
+ Assert.IsTrue(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should be an File account');
+ Assert.AreEqual(AccountId, TempFileAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong connector');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountDefaultDifferentTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ AccountId: Guid;
+ DefaultAccountId: Guid;
+ begin
+ // [Scenario] When the File scenario and the default scenario are mapped to different File accounts, GetFileAccount returns the correct account
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] An File scenario is mapped to an account, the default scenario is mapped to another account
+ Initialize();
+ FileConnectorMock.AddAccount(AccountId);
+ FileConnectorMock.AddAccount(DefaultAccountId);
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::"Test File Scenario", AccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, DefaultAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+
+ // [When] calling GetFileAccount
+ // [Then] true is returned and the File accounts are as expected
+ Assert.IsTrue(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should be an File account');
+ Assert.AreEqual(AccountId, TempFileAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong connector');
+
+ Assert.IsTrue(FileScenario.GetFileAccount(Enum::"File Scenario"::Default, TempFileAccount), 'There should be an File account');
+ Assert.AreEqual(DefaultAccountId, TempFileAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong connector');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountDefaultDifferentNotExistTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ DefaultAccountId: Guid;
+ NonExistingAccountId: Guid;
+ begin
+ // [Scenario] When the File scenario is mapped to a non-existing account and the default scenario is mapped to an existing accounts, GetFileAccount returns the correct account
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] An File scenario is mapped to a non-existing account, the default scenario is mapped to an existing account
+ Initialize();
+ FileConnectorMock.AddAccount(DefaultAccountId);
+ NonExistingAccountId := Any.GuidValue();
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::"Test File Scenario", NonExistingAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, DefaultAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+
+ // [When] calling GetFileAccount
+ // [Then] true is returned and the File accounts are as expected
+ Assert.IsTrue(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should be an File account');
+ Assert.AreEqual(DefaultAccountId, TempFileAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong connector');
+
+ Assert.IsTrue(FileScenario.GetFileAccount(Enum::"File Scenario"::Default, TempFileAccount), 'There should be an File account for the default scenario');
+ Assert.AreEqual(DefaultAccountId, TempFileAccount."Account Id", 'Wrong default account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong default account connector');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure GetFileAccountDifferentDefaultNotExistTest()
+ var
+ TempFileAccount: Record "File Account" temporary;
+ AccountId: Guid;
+ DefaultAccountId: Guid;
+ begin
+ // [Scenario] When the File scenario is mapped to an existing account and the default scenario is mapped to a non-existing accounts, GetFileAccount returns the correct account
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] An File scenario is mapped to an existing account, the default scenario is mapped to a non-existing account
+ Initialize();
+ FileConnectorMock.AddAccount(AccountId);
+ DefaultAccountId := Any.GuidValue();
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::"Test File Scenario", AccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+ FileScenarioMock.AddMapping(Enum::"File Scenario"::Default, DefaultAccountId, Enum::"Ext. File Storage Connector"::"Test File Storage Connector");
+
+ // [When] calling GetFileAccount
+ // [Then] true is returned and the File account is as expected
+ Assert.IsTrue(FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount), 'There should be an File account');
+ Assert.AreEqual(AccountId, TempFileAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong connector');
+
+ // [Then] there's no account for the default File scenario
+ Assert.IsFalse(FileScenario.GetFileAccount(Enum::"File Scenario"::Default, TempFileAccount), 'There should not be an File account for the default scenario');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure SetFileAccountTest()
+ var
+ TempFileAccount, TempAnotherAccount : Record "File Account" temporary;
+ ExtFileStorageTestLib: Codeunit "Ext. File Storage Test Lib.";
+ ExternalFileStorageConnector: Interface "External File Storage Connector";
+ AccountId: Guid;
+ Scenario: Enum "File Scenario";
+ begin
+ // [Scenario] When SetAccount is called, the entry in the database is as expected
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] A random File account
+ Initialize();
+ TempFileAccount."Account Id" := Any.GuidValue();
+ TempFileAccount.Connector := Enum::"Ext. File Storage Connector"::"Test File Storage Connector";
+ Scenario := Scenario::Default;
+
+ // [When] Setting the File account for the scenario
+ FileScenario.SetFileAccount(Scenario, TempFileAccount);
+
+ // [Then] The scenario exists and is as expected
+ Assert.IsTrue(ExtFileStorageTestLib.GetFileScenarioAccountIdAndFileConnector(Scenario, AccountId, ExternalFileStorageConnector), 'The File scenario should exist');
+ Assert.AreEqual(AccountId, TempFileAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempFileAccount.Connector, 'Wrong connector');
+
+ TempAnotherAccount."Account Id" := Any.GuidValue();
+ TempAnotherAccount.Connector := Enum::"Ext. File Storage Connector"::"Test File Storage Connector";
+
+ // [When] Setting overwriting the File account for the scenario
+ FileScenario.SetFileAccount(Scenario, TempAnotherAccount);
+
+ // [Then] The scenario still exists and is as expected
+ Assert.IsTrue(ExtFileStorageTestLib.GetFileScenarioAccountIdAndFileConnector(Scenario, AccountId, ExternalFileStorageConnector), 'The File scenario should exist');
+ Assert.AreEqual(AccountId, TempAnotherAccount."Account Id", 'Wrong account ID');
+ Assert.AreEqual(Enum::"Ext. File Storage Connector"::"Test File Storage Connector", TempAnotherAccount.Connector, 'Wrong connector');
+ end;
+
+ [Test]
+ [Scope('OnPrem')]
+ procedure UnassignScenarioTest()
+ var
+ TempDefaultAccount, TempFileAccount, TempResultAccount : Record "File Account" temporary;
+ begin
+ // [Scenario] When unassigning a scenario then it falls back to the default account.
+ PermissionsMock.Set('File Storage Admin');
+
+ // [Given] Two accounts, one default and one not
+ Initialize();
+ FileConnectorMock.AddAccount(TempFileAccount);
+ FileConnectorMock.AddAccount(TempDefaultAccount);
+ FileScenario.SetDefaultFileAccount(TempDefaultAccount);
+ FileScenario.SetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempFileAccount);
+
+ // mid-test verification
+ FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempResultAccount);
+ Assert.AreEqual(TempFileAccount."Account Id", TempResultAccount."Account Id", 'Wrong account');
+
+ // [When] Unassign the File scenario
+ FileScenario.UnassignScenario(Enum::"File Scenario"::"Test File Scenario");
+
+ // [Then] The default account is returned for that account
+ FileScenario.GetFileAccount(Enum::"File Scenario"::"Test File Scenario", TempResultAccount);
+ Assert.AreEqual(TempDefaultAccount."Account Id", TempResultAccount."Account Id", 'The default account should have been returned');
+ end;
+
+ local procedure Initialize()
+ begin
+ FileScenarioMock.DeleteAllMappings();
+ end;
+}
\ No newline at end of file