├── src ├── Components │ ├── Loader.razor.css │ ├── Editor.razor.css │ ├── Loader.razor │ ├── Preview.razor │ ├── Menu.razor.css │ ├── FileDrop.razor.css │ ├── Menu.razor │ ├── Preview.razor.css │ ├── FileDrop.razor │ └── Editor.razor ├── wwwroot │ ├── favicon.ico │ ├── images │ │ ├── excel.png │ │ ├── github.png │ │ ├── dataverse.png │ │ ├── powerapps.png │ │ └── powerautomate.png │ ├── powernote-192.png │ ├── powernote-50.png │ ├── powernote-512.png │ ├── service-worker.js │ ├── manifest.json │ ├── service-worker.published.js │ ├── index.css │ ├── index.html │ └── scripts │ │ └── MonacoEditor.js ├── Modules │ ├── tsconfig.json │ └── MonacoEditor.ts ├── App.razor ├── Models │ ├── PowerFxSample.cs │ ├── ExcelWorkbook.cs │ └── PowerFxGrammar.cs ├── _Imports.razor ├── Program.cs ├── Properties │ ├── launchSettings.json │ └── ServiceDependencies │ │ └── MR365app - Web Deploy │ │ └── profile.arm.json ├── PowerNote.sln ├── PowerNote.csproj ├── Services │ ├── AppReader.cs │ ├── FormulaJsHost.cs │ ├── UrlManager.cs │ ├── YamlReader.cs │ ├── AppPreviewer.cs │ ├── FileManager.cs │ ├── ExcelReader.cs │ └── PowerFxHost.cs └── Pages │ ├── Index.razor.css │ └── Index.razor ├── .gitattributes ├── .github ├── images │ └── powernote.png ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── ghp-deploy.yml ├── lib └── Microsoft.PowerPlatform.Formulas.Tools.dll ├── LICENSE ├── README.md └── .gitignore /src/Components/Loader.razor.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/Components/Editor.razor.css: -------------------------------------------------------------------------------- 1 | #monaco-editor { 2 | width: 100%; 3 | height: 100%; 4 | } -------------------------------------------------------------------------------- /src/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/favicon.ico -------------------------------------------------------------------------------- /.github/images/powernote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/.github/images/powernote.png -------------------------------------------------------------------------------- /src/wwwroot/images/excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/images/excel.png -------------------------------------------------------------------------------- /src/wwwroot/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/images/github.png -------------------------------------------------------------------------------- /src/wwwroot/powernote-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/powernote-192.png -------------------------------------------------------------------------------- /src/wwwroot/powernote-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/powernote-50.png -------------------------------------------------------------------------------- /src/wwwroot/powernote-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/powernote-512.png -------------------------------------------------------------------------------- /src/wwwroot/images/dataverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/images/dataverse.png -------------------------------------------------------------------------------- /src/wwwroot/images/powerapps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/images/powerapps.png -------------------------------------------------------------------------------- /src/wwwroot/images/powerautomate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/src/wwwroot/images/powerautomate.png -------------------------------------------------------------------------------- /lib/Microsoft.PowerPlatform.Formulas.Tools.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalable-dynamics/PowerNote/HEAD/lib/Microsoft.PowerPlatform.Formulas.Tools.dll -------------------------------------------------------------------------------- /src/Components/Loader.razor: -------------------------------------------------------------------------------- 1 | @if (Loading) 2 | { 3 |
4 |
5 |
6 | } 7 | 8 | @code { 9 | 10 | [Parameter] 11 | public bool Loading { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); 5 | -------------------------------------------------------------------------------- /src/Modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": false, 4 | "noImplicitAny": false, 5 | "noEmitOnError": true, 6 | "removeComments": true, 7 | "sourceMap": false, 8 | "target": "ES2020", 9 | "module": "ES2020", 10 | "outDir": "../wwwroot/scripts" 11 | } 12 | } -------------------------------------------------------------------------------- /src/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Models/PowerFxSample.cs: -------------------------------------------------------------------------------- 1 | namespace PowerNote.Models; 2 | internal static class PowerFxSample 3 | { 4 | public static string Code = @" 5 | Set( Radius, 10 ) 6 | 7 | Pi = 3.14159265359 8 | 9 | Area = Pi * Radius * Radius 10 | 11 | Set( Radius, 300 ) 12 | 13 | Circumference = 2 * Pi * Radius 14 | 15 | Set( Radius, 40 ) 16 | 17 | Area 18 | 19 | Circumference 20 | 21 | "; 22 | } 23 | -------------------------------------------------------------------------------- /src/Components/Preview.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 🗙 6 | 7 | @( 8 | (MarkupString)Html 9 | ) 10 |
11 | 12 | @code { 13 | [Parameter] 14 | public string Html { get; set; } 15 | 16 | [Parameter] 17 | public Action OnClose { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using PowerNote 10 | @using PowerNote.Components 11 | @using PowerNote.Models 12 | @using PowerNote.Services 13 | @using Microsoft.Fast.Components.FluentUI 14 | -------------------------------------------------------------------------------- /src/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PowerNote", 3 | "short_name": "PowerNote", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "background_color": "#161A42", 7 | "theme_color": "#DADBE2", 8 | "prefer_related_applications": false, 9 | "icons": [ 10 | { 11 | "src": "powernote-512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | }, 15 | { 16 | "src": "powernote-192.png", 17 | "type": "image/png", 18 | "sizes": "192x192" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/Components/Menu.razor.css: -------------------------------------------------------------------------------- 1 | .sidebar-menu { 2 | flex: 1; 3 | order: 1; 4 | border-right: outset 4px white; 5 | background: #eee; 6 | } 7 | 8 | .sidebar-menu h3 { 9 | margin: 0; 10 | text-align: center; 11 | cursor: default; 12 | height: 30px; 13 | line-height: 30px; 14 | } 15 | 16 | .sidebar-menu .go-back { 17 | position: absolute; 18 | line-height: 30px; 19 | text-decoration: none; 20 | font-weight: bold; 21 | color: navy; 22 | } -------------------------------------------------------------------------------- /src/Components/FileDrop.razor.css: -------------------------------------------------------------------------------- 1 | #file-drop { 2 | position: absolute; 3 | inset: 0; 4 | } 5 | 6 | #file-drop form { 7 | position: absolute; 8 | inset: 0; 9 | cursor: default; 10 | } 11 | 12 | #file-drop b { 13 | color: white; 14 | margin-left: 4px; 15 | line-height: 30px; 16 | } 17 | 18 | #file-drop form:hover { 19 | border: dashed 4px blue; 20 | color: blue; 21 | } 22 | 23 | #file-drop label { 24 | position: absolute; 25 | inset: 0; 26 | background: transparent; 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the capability or change that you would like to introduce** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe the alternative approaches you have used or are considering** 14 | A summary of any alternative solutions or features you've considered. 15 | 16 | **Is your feature request related to a problem? Please describe.** 17 | A description of what the problem is. Ex. I'm always frustrated when [...] 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/Components/Menu.razor: -------------------------------------------------------------------------------- 1 | @typeparam TItem 2 | 3 | @if (Items?.Count() > 0) 4 | { 5 | 6 | @foreach (var item in Items) 7 | { 8 | 9 | @if(ItemTemplate != null) 10 | { 11 | @(ItemTemplate(item)) 12 | } 13 | else 14 | { 15 | @(item.ToString()) 16 | } 17 | 18 | } 19 | 20 | } 21 | 22 | @code { 23 | 24 | [Parameter] 25 | public Action OnSelect { get; set; } 26 | 27 | [Parameter] 28 | public IEnumerable Items { get; set; } 29 | 30 | [Parameter] 31 | public RenderFragment ItemTemplate { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 2 | using Microsoft.JSInterop; 3 | using PowerNote; 4 | using PowerNote.Services; 5 | 6 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 7 | builder.RootComponents.Add("#app"); 8 | 9 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 10 | builder.Services.AddSingleton(services => (IJSInProcessRuntime)services.GetRequiredService()); 11 | builder.Services.AddSingleton(); 12 | builder.Services.AddSingleton(); 13 | builder.Services.AddSingleton(); 14 | builder.Services.AddSingleton(); 15 | builder.Services.AddSingleton(); 16 | builder.Services.AddSingleton(); 17 | builder.Services.AddSingleton(); 18 | builder.Services.AddSingleton(); 19 | 20 | await builder.Build().RunAsync(); 21 | -------------------------------------------------------------------------------- /src/Components/Preview.razor.css: -------------------------------------------------------------------------------- 1 | 2 | .app-preview { 3 | position: fixed; 4 | inset: 20%; 5 | overflow: auto; 6 | background-color: #161A42; 7 | color: #DADBE2; 8 | border: solid 4px #45496D; 9 | } 10 | 11 | .app-preview *:first-child { 12 | position: fixed; 13 | width: 40px; 14 | height: 40px; 15 | line-height: 40px; 16 | margin-top: -20px; 17 | cursor: pointer; 18 | font-size: 20px; 19 | text-align: center; 20 | font-weight: bold; 21 | text-decoration: none; 22 | color: white; 23 | background-color: red; 24 | border-radius: 50%; 25 | } 26 | 27 | .app-preview .powerapp-component { 28 | position: relative; 29 | overflow: hidden; 30 | border: solid 1px rgba(44,0,255,100); 31 | white-space: nowrap; 32 | } 33 | 34 | .app-preview .powerapp-component:hover { 35 | overflow: auto; 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: scalabled 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected outcome** 21 | A description of what you expect to happen in this scenario 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | - App mode (PWA/offline) 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:14744", 7 | "sslPort": 44321 8 | } 9 | }, 10 | "profiles": { 11 | "PowerNote": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:7063;http://localhost:5063", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Scalable Dynamics, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/PowerNote.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.31903.286 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerNote", "PowerNote.csproj", "{A6B636F4-3028-4CAB-8A6D-906D713BCBE7}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A6B636F4-3028-4CAB-8A6D-906D713BCBE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A6B636F4-3028-4CAB-8A6D-906D713BCBE7}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A6B636F4-3028-4CAB-8A6D-906D713BCBE7}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A6B636F4-3028-4CAB-8A6D-906D713BCBE7}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {B13E5A56-16DD-4FD0-8E4D-3A1D22B0831A} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/PowerNote.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | disable 6 | enable 7 | service-worker-assets.js 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | ..\lib\Microsoft.PowerPlatform.Formulas.Tools.dll 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Components/FileDrop.razor: -------------------------------------------------------------------------------- 1 | @inject IJSInProcessRuntime JSRuntime 2 | @inject FileManager Files 3 | @using System.IO 4 | 5 |
6 | 7 | 8 | 9 | @if (!Loading) 10 | { 11 | @ChildContent 12 | } 13 | 14 |
15 | 18 |
19 | 20 |
21 | 22 | @code { 23 | [Parameter] 24 | public RenderFragment ChildContent { get; set; } 25 | 26 | [Parameter] 27 | public Action OnChange { get; set; } 28 | 29 | public bool Loading; 30 | 31 | private async Task OnInputFileChange(InputFileChangeEventArgs e) 32 | { 33 | Loading = true; 34 | StateHasChanged(); 35 | 36 | Console.WriteLine("OnInputFileChange: " + e.FileCount); 37 | if(e.FileCount > 10) 38 | { 39 | Console.WriteLine("WARNING: Too many files were added: " + e.FileCount); 40 | } 41 | 42 | var selectedFiles = e.GetMultipleFiles(); 43 | var tooLarge = selectedFiles.Where(f => f.Size > 512_000).Select(f => f.Name).ToArray(); 44 | 45 | if(tooLarge.Any()) 46 | { 47 | Console.WriteLine("WARNING: Files larger than 512000 bytes are too large; {0} files will be skipped", tooLarge.Length); 48 | } 49 | 50 | await Files.UploadFilesAsync(selectedFiles.Where(f => !tooLarge.Contains(f.Name)).Take(10).ToArray()); 51 | Files.ReadAppsFolder(); 52 | OnChange(); 53 | 54 | Loading = false; 55 | StateHasChanged(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Components/Editor.razor: -------------------------------------------------------------------------------- 1 | @inject IJSInProcessRuntime JSRuntime 2 | @implements IAsyncDisposable 3 | 4 |
5 | 6 | @code { 7 | private Task _monacoEditor; 8 | private Task MonacoEditor => _monacoEditor ??= JSRuntime.InvokeAsync("import", $"./scripts/MonacoEditor.js?{DateTime.UtcNow.ToFileTime()}").AsTask(); 9 | 10 | [Parameter] 11 | public string Code { get; set; } 12 | 13 | [Parameter] 14 | public string OnChange { get; set; } 15 | 16 | [Parameter] 17 | public string OnHover { get; set; } 18 | 19 | [Parameter] 20 | public string OnSuggest { get; set; } 21 | 22 | [Parameter] 23 | public string OnSave { get; set; } 24 | 25 | [Parameter] 26 | public string OnExecute { get; set; } 27 | 28 | [Parameter] 29 | public string OnPreview { get; set; } 30 | 31 | protected override async Task OnAfterRenderAsync(bool firstRender) 32 | { 33 | if (firstRender) 34 | { 35 | var monacoEditor = await MonacoEditor; 36 | await monacoEditor.InvokeVoidAsync("loadMonacoEditor", CancellationToken.None, "monaco-editor", Code, OnChange, OnHover, OnSuggest, OnSave, OnExecute, OnPreview); 37 | } 38 | else 39 | { 40 | var monacoEditor = await MonacoEditor; 41 | await monacoEditor.InvokeVoidAsync("updateMonacoEditor", CancellationToken.None, Code); 42 | } 43 | } 44 | 45 | public async ValueTask DisposeAsync() 46 | { 47 | if (_monacoEditor != null) { 48 | var monacoEditor = await _monacoEditor; 49 | await monacoEditor.DisposeAsync(); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerNote 2 | 3 | ![PowerNote](https://raw.githubusercontent.com/scalable-dynamics/PowerNote/main/.github/images/powernote.png) 4 | 5 | Power Fx editor and file viewer 6 | 7 | [Online Version (GitHub Pages)](https://scalable-dynamics.github.io/PowerNote/) 8 | 9 | ### Features 10 | - Create Power Fx formulas and check/eval the result 11 | - View and edit Power Fx formulas from various file types (.msapp, .json, .xlsx, .zip) 12 | * Canvas Apps (.msapp) 13 | * Cloud Flows (.json) 14 | * Excel Documents (.xlsx) 15 | * Dataverse Solutions (.zip) 16 | - Save, share and collaborate by sending a PowerNote url 17 | * Press CTRL+S to save while in the editor, this will set the url so you can share with friends! 18 | * Selecting an app or file will change the url to that file 19 | * The url will contain a base64-encoded representation of the file or code - all data after the '#' in the url will not be sent to the server 20 | 21 | ### Powered by 22 | * [Microsoft.AspNetCore.Components.WebAssembly](https://blazor.net) 23 | * [Microsoft.Fast.Components.FluentUI](https://www.nuget.org/packages/Microsoft.Fast.Components.FluentUI) 24 | * [Microsoft.PowerFx.Core](https://www.nuget.org/packages/Microsoft.PowerFx.Core) 25 | * [Microsoft.PowerFx.Interpreter](https://www.nuget.org/packages/Microsoft.PowerFx.Interpreter) 26 | * [Microsoft.PowerPlatform.Formulas.Tools](https://github.com/microsoft/PowerApps-Language-Tooling) 27 | * [Microsoft Monaco Editor](https://microsoft.github.io/monaco-editor/index.html) 28 | * [Formula JS](https://formulajs.info/) 29 | 30 | ## License 31 | 32 | [MIT License](https://github.com/scalable-dynamics/PowerNote/blob/master/LICENSE) 33 | 34 | *PowerNote* is licensed under the 35 | [MIT](https://github.com/scalable-dynamics/PowerNote/blob/master/LICENSE) license 36 | -------------------------------------------------------------------------------- /src/wwwroot/service-worker.published.js: -------------------------------------------------------------------------------- 1 | // Caution! Be sure you understand the caveats before publishing an application with 2 | // offline support. See https://aka.ms/blazor-offline-considerations 3 | 4 | self.importScripts('./service-worker-assets.js'); 5 | self.addEventListener('install', event => event.waitUntil(onInstall(event))); 6 | self.addEventListener('activate', event => event.waitUntil(onActivate(event))); 7 | self.addEventListener('fetch', event => event.respondWith(onFetch(event))); 8 | 9 | const cacheNamePrefix = 'offline-cache-'; 10 | const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; 11 | const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; 12 | const offlineAssetsExclude = [ /^service-worker\.js$/ ]; 13 | 14 | async function onInstall(event) { 15 | console.info('Service worker: Install'); 16 | 17 | // Fetch and cache all matching items from the assets manifest 18 | const assetsRequests = self.assetsManifest.assets 19 | .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) 20 | .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) 21 | .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); 22 | await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); 23 | } 24 | 25 | async function onActivate(event) { 26 | console.info('Service worker: Activate'); 27 | 28 | // Delete unused caches 29 | const cacheKeys = await caches.keys(); 30 | await Promise.all(cacheKeys 31 | .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) 32 | .map(key => caches.delete(key))); 33 | } 34 | 35 | async function onFetch(event) { 36 | let cachedResponse = null; 37 | if (event.request.method === 'GET') { 38 | // For all navigation requests, try to serve index.html from cache 39 | // If you need some URLs to be server-rendered, edit the following check to exclude those URLs 40 | const shouldServeIndexHtml = event.request.mode === 'navigate'; 41 | 42 | const request = shouldServeIndexHtml ? 'index.html' : event.request; 43 | const cache = await caches.open(cacheName); 44 | cachedResponse = await cache.match(request); 45 | } 46 | 47 | return cachedResponse || fetch(event.request); 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/ghp-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - "src/**" 8 | 9 | jobs: 10 | deploy-to-github-pages: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup .NET SDK 16 | uses: actions/setup-dotnet@v1.8.1 17 | with: 18 | dotnet-version: 6.0.x 19 | 20 | - name: Publish .NET Blazor Project 21 | run: dotnet publish src/PowerNote.csproj -c Release -o release --nologo 22 | 23 | - name: Change base-tag in index.html from / to PowerNote for the GitHub Pages url scheme 24 | run: sed -i 's///g' release/wwwroot/index.html 25 | 26 | # credit: https://github.com/Swimburger/BlazorGitHubPagesDemo/blob/pwa/.github/workflows/pwa.yml 27 | - name: Fix service-worker-assets.js hashes 28 | working-directory: release/wwwroot 29 | run: | 30 | jsFile=$( service-worker-assets.js 57 | 58 | # add .nojekyll file to tell GitHub pages to not treat this as a Jekyll project. (Allow files and folders starting with an underscore) 59 | - name: Add .nojekyll file 60 | run: touch release/wwwroot/.nojekyll 61 | 62 | - name: Commit wwwroot to GitHub Pages 63 | uses: JamesIves/github-pages-deploy-action@3.7.1 64 | with: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | BRANCH: ghp-app 67 | FOLDER: release/wwwroot 68 | -------------------------------------------------------------------------------- /src/Models/ExcelWorkbook.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace PowerNote.Models; 4 | 5 | [Serializable] 6 | [XmlRoot("workbook", Namespace = "http://schemas.openxmlformats.org/spreadsheetml/2006/main")] 7 | public class ExcelWorkbook 8 | { 9 | [XmlArray("sheets")] 10 | [XmlArrayItem("sheet")] 11 | public ExcelWorkbookSheet[] Sheets { get; set; } 12 | 13 | [XmlArray("definedNames")] 14 | [XmlArrayItem("definedName")] 15 | public ExcelWorkbookDefinedName[] DefinedNames { get; set; } 16 | } 17 | 18 | public class ExcelWorkbookDefinedName 19 | { 20 | [XmlAttribute("name")] 21 | public string Name { get; set; } 22 | 23 | [XmlElement] 24 | public string Text { get; set; } 25 | } 26 | 27 | public class ExcelWorkbookSheet 28 | { 29 | [XmlAttribute("name")] 30 | public string Name { get; set; } 31 | 32 | [XmlAttribute("sheetId")] 33 | public string SheetId { get; set; } 34 | 35 | [XmlAttribute("id", Namespace = "http://schemas.openxmlformats.org/officeDocument/2006/relationships")] 36 | public string SheetReferenceId { get; set; } 37 | } 38 | 39 | [Serializable] 40 | [XmlRoot("sst", Namespace = "http://schemas.openxmlformats.org/spreadsheetml/2006/main")] 41 | public class ExcelWorkbookStringTable 42 | { 43 | [XmlAttribute("uniqueCount")] 44 | public string UniqueCount { get; set; } 45 | 46 | [XmlAttribute("count")] 47 | public string Count { get; set; } 48 | 49 | [XmlElement("si")] 50 | public ExcelWorkbookStringTableText[] Items { get; set; } 51 | } 52 | 53 | public class ExcelWorkbookStringTableText 54 | { 55 | [XmlElement("t")] 56 | public string Text { get; set; } 57 | } 58 | 59 | [Serializable] 60 | [XmlRoot("worksheet", Namespace = "http://schemas.openxmlformats.org/spreadsheetml/2006/main")] 61 | public class ExcelWorksheet 62 | { 63 | [XmlArray("sheetData")] 64 | [XmlArrayItem("row")] 65 | public ExcelWorksheetRow[] Rows { get; set; } 66 | 67 | [XmlArray("mergeCells")] 68 | [XmlArrayItem("mergeCell")] 69 | public ExcelWorksheetMergedCell[] MergedCells { get; set; } 70 | } 71 | 72 | public class ExcelWorksheetCell 73 | { 74 | [XmlAttribute("r")] 75 | public string CellReference { get; set; } 76 | 77 | [XmlAttribute("t")] 78 | public string CellType { get; set; } 79 | 80 | [XmlElement("v")] 81 | public string Value { get; set; } 82 | 83 | [XmlElement("f")] 84 | public string Formula { get; set; } 85 | } 86 | 87 | public class ExcelWorksheetMergedCell 88 | { 89 | [XmlAttribute("ref")] 90 | public string CellRange { get; set; } 91 | } 92 | 93 | public class ExcelWorksheetRow 94 | { 95 | [XmlAttribute("r")] 96 | public int RowReference { get; set; } 97 | 98 | [XmlElement("c")] 99 | public ExcelWorksheetCell[] Cells { get; set; } 100 | } -------------------------------------------------------------------------------- /src/wwwroot/index.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | font-family: sans-serif; 4 | background-color: #161A42; 5 | color: #DADBE2; 6 | overflow: hidden; 7 | } 8 | 9 | #blazor-error-ui { 10 | background: lightyellow; 11 | bottom: 0; 12 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 13 | display: none; 14 | left: 0; 15 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 16 | position: fixed; 17 | width: 100%; 18 | z-index: 1000; 19 | } 20 | 21 | #blazor-error-ui .dismiss { 22 | cursor: pointer; 23 | position: absolute; 24 | right: 0.75rem; 25 | top: 0.5rem; 26 | } 27 | 28 | a { 29 | color: #DADBE2 !important; 30 | text-decoration: none; 31 | } 32 | 33 | a:hover { 34 | color: rgba(44,0,255,100) !important; 35 | text-decoration: underline; 36 | } 37 | 38 | .powerfx-type { 39 | color: rgba(44,0,255,100) !important; 40 | font-weight: bold; 41 | } 42 | 43 | .powerfx-error { 44 | color: rgba(255,40,0,145) !important; 45 | text-decoration: underline; 46 | } 47 | 48 | .app-start { 49 | position: relative; 50 | margin: 20px auto; 51 | padding: 20px; 52 | width: 450px; 53 | background-color: #0B0D21; 54 | border: solid 4px #45496D; 55 | } 56 | 57 | .app-start dt { 58 | font-weight: bold; 59 | color: white; 60 | margin: 10px 0; 61 | } 62 | 63 | .app-start dd a { 64 | color: white; 65 | } 66 | 67 | h1 { 68 | color: white; 69 | } 70 | 71 | h1 img { 72 | width: 40px; 73 | vertical-align: middle; 74 | margin: 0 4px; 75 | } 76 | 77 | dl { 78 | font-style: italic; 79 | } 80 | 81 | dt b { 82 | font-style: normal; 83 | } 84 | 85 | dd img { 86 | width: 30px; 87 | vertical-align: middle; 88 | margin-right: 4px; 89 | } 90 | 91 | .app-loader { 92 | position: fixed; 93 | inset: 0; 94 | background-color: #1E1E1E; 95 | opacity: .3; 96 | z-index:100; 97 | } 98 | 99 | .app-loader .ripple { 100 | display: inline-block; 101 | position: relative; 102 | width: 120px; 103 | height: 120px; 104 | margin: 300px 50%; 105 | } 106 | 107 | .app-loader .ripple:before, 108 | .app-loader .ripple:after { 109 | content: ''; 110 | position: absolute; 111 | border: 4px solid #45496D; 112 | opacity: 1; 113 | border-radius: 50%; 114 | animation: ripple-loader 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; 115 | } 116 | 117 | .app-loader .ripple:after { 118 | animation-delay: -0.7s; 119 | } 120 | 121 | @keyframes ripple-loader { 122 | 0% { 123 | top: 56px; 124 | left: 56px; 125 | width: 0; 126 | height: 0; 127 | opacity: 1; 128 | } 129 | 130 | 100% { 131 | top: 0px; 132 | left: 0px; 133 | width: 112px; 134 | height: 112px; 135 | opacity: 0; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Services/AppReader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.PowerPlatform.Formulas.Tools; 2 | 3 | namespace PowerNote.Services; 4 | 5 | public record PowerApp(string Name, PowerAppComponent[] Components, string FilePath) 6 | { 7 | public override string ToString() => Name; 8 | } 9 | 10 | public record PowerAppComponent(string Name, Dictionary Types, PowerAppComponentObject[] Objects) 11 | { 12 | public PowerAppComponent(string name, Dictionary types, Dictionary> objects) 13 | : this(name, types, objects.Select(o => new PowerAppComponentObject(o.Key, o.Value)).ToArray()) {} 14 | 15 | public override string ToString() => Name; 16 | } 17 | 18 | public record PowerAppComponentObject(string Name, Dictionary Data) 19 | { 20 | public override string ToString() => Name; 21 | } 22 | 23 | public class AppReader 24 | { 25 | private readonly YamlReader _yamlReader; 26 | 27 | public AppReader(YamlReader yamlReader) 28 | { 29 | _yamlReader = yamlReader; 30 | } 31 | 32 | public PowerApp ReadApp(string msappFilePath) 33 | { 34 | var name = Path.GetFileNameWithoutExtension(msappFilePath); 35 | var components = ReadAppComponents(name, File.OpenRead(msappFilePath)).ToArray(); 36 | return new(name, components, msappFilePath); 37 | } 38 | 39 | public IEnumerable ReadAppComponents(string msappFilePath) 40 | { 41 | var name = Path.GetFileNameWithoutExtension(msappFilePath); 42 | return ReadAppComponents(name, File.OpenRead(msappFilePath)); 43 | } 44 | 45 | private IEnumerable ReadAppComponents(string name, Stream msappFileStream) 46 | { 47 | (CanvasDocument msApp, ErrorContainer errors) = CanvasDocument.LoadFromMsapp(msappFileStream); 48 | errors.Write(Console.Error); 49 | 50 | if (msApp == null || errors.HasErrors) 51 | { 52 | errors.Write(Console.Error); 53 | Console.WriteLine("*** ReadAppComponents: CanvasDocument {0} could not be read.", name); 54 | yield break; 55 | } 56 | 57 | var info = Directory.CreateDirectory(name); 58 | errors = msApp.SaveToSources(info.FullName, verifyOriginalPath: null); 59 | if (errors.HasErrors) 60 | { 61 | errors.Write(Console.Error); 62 | yield break; 63 | } 64 | 65 | var files = Directory.EnumerateFiles(info.FullName, "*.yaml", SearchOption.AllDirectories).ToArray(); 66 | Console.WriteLine("*** CanvasDocument {0} contains {1} files", name, files.Length); 67 | foreach (var file in files) 68 | { 69 | if (!file.Contains("/tests/", StringComparison.InvariantCultureIgnoreCase)) 70 | { 71 | Console.WriteLine(file); 72 | var component = Path.GetFileNameWithoutExtension(file); 73 | var data = _yamlReader.ReadYaml(file); 74 | yield return new(component, data.Types, data.Objects); 75 | Console.WriteLine(); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Services/FormulaJsHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using Microsoft.PowerFx; 3 | using Microsoft.PowerFx.Core.Public.Types; 4 | using Microsoft.PowerFx.Core.Public.Values; 5 | 6 | namespace PowerNote.Services; 7 | 8 | public class FormulaJsHost 9 | { 10 | private static string[] StringFunctions = new[] { 11 | "Find", 12 | "Match", 13 | "Proper", 14 | "Search", 15 | "Weekday" 16 | }; 17 | 18 | private static string[] NumericFunctions = new[] { 19 | "Acos", 20 | "Acot", 21 | "Asin", 22 | "Atan", 23 | "Atan2", 24 | "Cos", 25 | "Cot", 26 | "Count", 27 | "CountA", 28 | "Degrees", 29 | "ISOWeekNum", 30 | "Pi", 31 | "Radians", 32 | "Rand", 33 | "Sin", 34 | "StdevP", 35 | "Tan", 36 | "VarP", 37 | "WeekNum" 38 | }; 39 | 40 | private readonly IJSRuntime _jSRuntime; 41 | private IJSInProcessRuntime _jSInProcessRuntime; 42 | private IJSInProcessRuntime JSInProcessRuntime 43 | { 44 | get { 45 | if (_jSInProcessRuntime == null) 46 | { 47 | _jSInProcessRuntime = _jSRuntime as IJSInProcessRuntime; 48 | if (_jSInProcessRuntime == null) 49 | { 50 | throw new InvalidOperationException("IJSInProcessRuntime not available"); 51 | } 52 | } 53 | return _jSInProcessRuntime; 54 | } 55 | } 56 | 57 | public FormulaJsHost(IJSRuntime jSRuntime) 58 | { 59 | _jSRuntime = jSRuntime; 60 | } 61 | 62 | public void AddFunctions(RecalcEngine engine) 63 | { 64 | var functions = engine.GetAllFunctionNames().ToArray(); 65 | foreach (var function in StringFunctions) 66 | { 67 | if (!functions.Contains(function)) 68 | { 69 | engine.AddFunction(new FormulaJsFunction(function, FormulaType.String, (args) => 70 | JSInProcessRuntime.Invoke($"formulajs.{function.ToUpper()}", args) 71 | )); 72 | } 73 | } 74 | foreach (var function in NumericFunctions) 75 | { 76 | if (!functions.Contains(function)) 77 | { 78 | engine.AddFunction(new FormulaJsFunction(function, FormulaType.Number, (args) => 79 | JSInProcessRuntime.Invoke($"formulajs.{function.ToUpper()}", args) 80 | )); 81 | } 82 | } 83 | } 84 | 85 | private class FormulaJsFunction : ReflectionFunction 86 | { 87 | private readonly Func _func; 88 | 89 | public FormulaJsFunction(string name, FormulaType type, Func func) : base(name, type) 90 | { 91 | _func = func; 92 | } 93 | 94 | public FormulaValue Execute(FormulaValue[] args) 95 | { 96 | var value = _func(args.Select(a => a.ToObject()).ToArray()); 97 | return FormulaValue.New(value, typeof(T)); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PowerNote 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 69 | 70 |
71 | An unhandled error has occurred. 72 | Reload 73 | 🗙 74 |
75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/Services/UrlManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.JSInterop; 3 | 4 | namespace PowerNote.Services; 5 | 6 | public class UrlManager 7 | { 8 | public static string CreateUrl(byte[] bytes, string type = "") 9 | { 10 | var data = Convert.ToBase64String(bytes); 11 | if (string.IsNullOrEmpty(type)) 12 | { 13 | return $"#{data}"; 14 | } 15 | else 16 | { 17 | return $"#{type}:{data}"; 18 | } 19 | } 20 | 21 | private readonly NavigationManager _navigationManager; 22 | private readonly FileManager _fileManager; 23 | private readonly IJSInProcessRuntime _jsRuntime; 24 | 25 | public UrlManager(NavigationManager navigationManager, FileManager fileManager, IJSInProcessRuntime jsRuntime) 26 | { 27 | _navigationManager = navigationManager; 28 | _fileManager = fileManager; 29 | _jsRuntime = jsRuntime; 30 | } 31 | 32 | public void ReadUrl(Action onFilesAdded) 33 | { 34 | var fragment = new Uri(_navigationManager.Uri, UriKind.Absolute).Fragment; 35 | if (fragment.StartsWith('#')) 36 | { 37 | try 38 | { 39 | var data = fragment.TrimStart('#'); 40 | var index = data.IndexOf(':'); 41 | if (index > -1) 42 | { 43 | var type = data.Substring(0, index); 44 | var file = data.Substring(index + 1); 45 | var bytes = Convert.FromBase64String(file); 46 | if (type == "msapp") 47 | { 48 | Console.WriteLine("Reading App File..."); 49 | _fileManager.WriteApp("App", bytes); 50 | onFilesAdded(); 51 | } 52 | else if (type == "zip") 53 | { 54 | Console.WriteLine("Reading Zip File..."); 55 | _fileManager.SaveAppPackage(bytes); 56 | onFilesAdded(); 57 | } 58 | else if (type == "xlsx") 59 | { 60 | Console.WriteLine("Reading Excel File..."); 61 | _fileManager.WriteExcel("Doc", bytes); 62 | onFilesAdded(); 63 | } 64 | else if (type == "json") 65 | { 66 | Console.WriteLine("Reading Flow File..."); 67 | _fileManager.WriteFlow("Flow", bytes); 68 | onFilesAdded(); 69 | } 70 | } 71 | else 72 | { 73 | Console.WriteLine("Reading Raw Text..."); 74 | var bytes = Convert.FromBase64String(data); 75 | _fileManager.WriteCode("Code", bytes); 76 | onFilesAdded(); 77 | } 78 | } 79 | catch (Exception ex) 80 | { 81 | Console.WriteLine("Error Reading Url Fragment: " + ex.ToString()); 82 | SetUrl(_navigationManager.BaseUri); 83 | } 84 | } 85 | } 86 | 87 | public void SetUrl(string url) 88 | { 89 | _jsRuntime.InvokeVoid("location.replace", url); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Services/YamlReader.cs: -------------------------------------------------------------------------------- 1 | namespace PowerNote.Services; 2 | 3 | public record YamlObject(Dictionary Types, Dictionary> Objects); 4 | public class YamlReader 5 | { 6 | const string tabs = " "; 7 | 8 | public YamlObject ReadYaml(string yamlFile) 9 | { 10 | var data = new Dictionary>(); 11 | var types = new Dictionary(); 12 | var yaml = File.ReadAllLines(yamlFile); 13 | var prev = ""; 14 | var path = ""; 15 | var parentProperty = ""; 16 | var parentData = new Dictionary(); 17 | var value = false; 18 | var level = 0; 19 | foreach (var line in yaml) 20 | { 21 | var depth = 0; 22 | var indent = tabs; 23 | var text = line; 24 | var size = tabs.Length; 25 | while (text.StartsWith(tabs)) 26 | { 27 | if (value && depth > level) 28 | { 29 | break; 30 | } 31 | indent += tabs; 32 | depth++; 33 | text = text.Substring(size); 34 | } 35 | if (value && depth <= level) 36 | { 37 | parentData[parentProperty] = prev.TrimStart('\r', '\n'); 38 | parentProperty = ""; 39 | value = false; 40 | } 41 | else if (value) 42 | { 43 | prev = prev + Environment.NewLine + text; 44 | } 45 | else 46 | { 47 | text = text.TrimEnd(':', ' ', '|', '-', '+'); 48 | if (depth < level) 49 | { 50 | level = depth + 1; 51 | path = string.Join('/', path.Split('/').Reverse().Skip(1).Reverse()); 52 | } 53 | if (line.TrimEnd().EndsWith(':')) 54 | { 55 | var asIndex = text.IndexOf(" As "); 56 | var asType = "?"; 57 | if (asIndex > -1) 58 | { 59 | asType = text.Substring(asIndex + 4).Trim('"'); 60 | text = text.Substring(0, asIndex).Trim('"'); 61 | } 62 | var suffix = "/" + text; 63 | if (!path.EndsWith(suffix)) 64 | { 65 | path += suffix; 66 | types[path] = asType; 67 | data[path] = parentData = new(); 68 | } 69 | level = depth + 1; 70 | } 71 | else if (text.Contains(':') && text.Contains('=')) 72 | { 73 | var valueIndex = text.IndexOf(':'); 74 | var valueText = text.Substring(valueIndex + 1); 75 | text = text.Substring(0, valueIndex); 76 | parentData[text] = valueText.Trim().TrimStart('='); 77 | } 78 | if (line.EndsWith("|-") || line.EndsWith("|+") || line.EndsWith("|")) 79 | { 80 | parentProperty = text; 81 | prev = ""; 82 | value = true; 83 | } 84 | else 85 | { 86 | prev = text; 87 | } 88 | } 89 | } 90 | return new(types, data); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Pages/Index.razor.css: -------------------------------------------------------------------------------- 1 | .power-note-app { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | inset: 0; 6 | position: fixed; 7 | background-color: #0B0D21; 8 | } 9 | 10 | .power-note-app header { 11 | flex: 1; 12 | display: flex; 13 | flex-direction: row; 14 | order: 1; 15 | max-height: 70px; 16 | background-color: #22264C; 17 | } 18 | 19 | .power-note-app header .title { 20 | flex: 5; 21 | order: 1; 22 | margin: 0; 23 | line-height: 70px; 24 | cursor: default; 25 | } 26 | 27 | .power-note-app header .files { 28 | flex: 2; 29 | order: 2; 30 | position: relative; 31 | overflow: hidden; 32 | } 33 | 34 | .power-note-app header .files .file-types dl { 35 | margin: 0; 36 | } 37 | 38 | .power-note-app header .files .file-types dt { 39 | margin: 10px 0; 40 | } 41 | 42 | .power-note-app header .files.full-screen { 43 | inset: 0; 44 | position: fixed; 45 | z-index: 1; 46 | } 47 | 48 | .power-note-app header .files.full-screen .file-types { 49 | position: relative; 50 | margin: 50px auto; 51 | padding: 0 30px; 52 | height: 250px; 53 | width: 350px; 54 | background-color: #0B0D21; 55 | border: solid 4px #45496D; 56 | } 57 | 58 | .power-note-app header .files.full-screen .file-types img { 59 | width: 40px; 60 | } 61 | 62 | .power-note-app header .files.full-screen .file-types i { 63 | font-size: .8em; 64 | line-height: 30px; 65 | color: #3E3E42; 66 | } 67 | 68 | .power-note-app header .files.top-right .file-types img { 69 | width: 20px; 70 | } 71 | 72 | .power-note-app header .files.top-right .file-types dl { 73 | text-align: right; 74 | } 75 | 76 | .power-note-app header .files.top-right .file-types dt { 77 | margin-right: 10px; 78 | font-size: .9em; 79 | } 80 | 81 | .power-note-app header .files.top-right .file-types dd { 82 | float: right; 83 | margin-right: 10px; 84 | margin-inline-start: 0; 85 | white-space: nowrap; 86 | font-size: .8em; 87 | } 88 | 89 | .power-note-app header .files.top-right .file-types i { 90 | display: none; 91 | } 92 | 93 | .power-note-app .app-object { 94 | flex: 1; 95 | display: flex; 96 | flex-direction: row; 97 | } 98 | 99 | .power-note-app .app-object span { 100 | flex: 10; 101 | flex-grow: inherit; 102 | line-height: 32px; 103 | } 104 | 105 | .power-note-app .app-object *:last-child { 106 | flex: 1; 107 | width: 30px; 108 | } 109 | 110 | .power-note-app main { 111 | flex: 10; 112 | display: flex; 113 | flex-direction: row; 114 | order: 2; 115 | } 116 | 117 | .power-note-app main aside { 118 | flex: 1; 119 | order: 1; 120 | min-width: 250px; 121 | max-width: 350px; 122 | background-color: #0B0D21; 123 | border-right: solid 4px #45496D; 124 | overflow: auto; 125 | max-height: 100vh; 126 | } 127 | 128 | .power-note-app main .content { 129 | flex: 10; 130 | order: 2; 131 | } 132 | -------------------------------------------------------------------------------- /src/Properties/ServiceDependencies/MR365app - Web Deploy/profile.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_dependencyType": "compute.appService.windows" 6 | }, 7 | "parameters": { 8 | "resourceGroupName": { 9 | "type": "string", 10 | "defaultValue": "MR365app-rg", 11 | "metadata": { 12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 13 | } 14 | }, 15 | "resourceGroupLocation": { 16 | "type": "string", 17 | "defaultValue": "eastus", 18 | "metadata": { 19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." 20 | } 21 | }, 22 | "resourceName": { 23 | "type": "string", 24 | "defaultValue": "MR365app", 25 | "metadata": { 26 | "description": "Name of the main resource to be created by this template." 27 | } 28 | }, 29 | "resourceLocation": { 30 | "type": "string", 31 | "defaultValue": "[parameters('resourceGroupLocation')]", 32 | "metadata": { 33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 34 | } 35 | } 36 | }, 37 | "variables": { 38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" 40 | }, 41 | "resources": [ 42 | { 43 | "type": "Microsoft.Resources/resourceGroups", 44 | "name": "[parameters('resourceGroupName')]", 45 | "location": "[parameters('resourceGroupLocation')]", 46 | "apiVersion": "2019-10-01" 47 | }, 48 | { 49 | "type": "Microsoft.Resources/deployments", 50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 51 | "resourceGroup": "[parameters('resourceGroupName')]", 52 | "apiVersion": "2019-10-01", 53 | "dependsOn": [ 54 | "[parameters('resourceGroupName')]" 55 | ], 56 | "properties": { 57 | "mode": "Incremental", 58 | "template": { 59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 60 | "contentVersion": "1.0.0.0", 61 | "resources": [ 62 | { 63 | "location": "[parameters('resourceLocation')]", 64 | "name": "[parameters('resourceName')]", 65 | "type": "Microsoft.Web/sites", 66 | "apiVersion": "2015-08-01", 67 | "tags": { 68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" 69 | }, 70 | "dependsOn": [ 71 | "[variables('appServicePlan_ResourceId')]" 72 | ], 73 | "kind": "app", 74 | "properties": { 75 | "name": "[parameters('resourceName')]", 76 | "kind": "app", 77 | "httpsOnly": true, 78 | "reserved": false, 79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]", 80 | "siteConfig": { 81 | "metadata": [ 82 | { 83 | "name": "CURRENT_STACK", 84 | "value": "dotnetcore" 85 | } 86 | ] 87 | } 88 | }, 89 | "identity": { 90 | "type": "SystemAssigned" 91 | } 92 | }, 93 | { 94 | "location": "[parameters('resourceLocation')]", 95 | "name": "[variables('appServicePlan_name')]", 96 | "type": "Microsoft.Web/serverFarms", 97 | "apiVersion": "2015-08-01", 98 | "sku": { 99 | "name": "S1", 100 | "tier": "Standard", 101 | "family": "S", 102 | "size": "S1" 103 | }, 104 | "properties": { 105 | "name": "[variables('appServicePlan_name')]" 106 | } 107 | } 108 | ] 109 | } 110 | } 111 | } 112 | ] 113 | } -------------------------------------------------------------------------------- /src/Services/AppPreviewer.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace PowerNote.Services; 6 | 7 | public record PowerAppPreview(string Name, string Component, string Html, string Code); 8 | public class AppPreviewer 9 | { 10 | private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true }; 11 | 12 | private static readonly string[] _mappedProperties = new string[] 13 | { 14 | "X", 15 | "Y", 16 | "Height", 17 | "Width", 18 | "ZIndex", 19 | "Size", 20 | "Text", 21 | "Fill", 22 | "Color", 23 | "BorderColor", 24 | "BorderThickness" 25 | }; 26 | 27 | public PowerAppPreview GetAppComponentPreview(PowerApp app, PowerAppComponent component) 28 | { 29 | var code = new StringBuilder(); 30 | code.AppendLine(); 31 | 32 | var html = new StringBuilder(); 33 | html.AppendLine("
"); 34 | 35 | foreach (var obj in component.Objects) 36 | { 37 | if (!obj.Name.StartsWith("Screen")) 38 | { 39 | code.AppendLine(GetObjectTable(component, obj)); 40 | html.AppendLine(GetObjectPreview(component, obj)); 41 | } 42 | } 43 | 44 | html.AppendLine("
"); 45 | 46 | var preview = html.ToString(); 47 | var powerfx = code.ToString(); 48 | Console.WriteLine("SetOutput: " + preview); 49 | Console.WriteLine("SetCode: " + powerfx); 50 | return new(app.Name, component.Name, preview, powerfx); 51 | } 52 | 53 | public string GetObjectTable(PowerAppComponent component, PowerAppComponentObject obj) 54 | { 55 | var code = new StringBuilder(); 56 | code.AppendLine(); 57 | 58 | var type = component.Types.TryGetValue(obj.Name, out var t) ? t : "Cell"; 59 | var properties = obj.Data 60 | .Where(p => p.Key != "OnSelect" && p.Key != "Text" && !_mappedProperties.Contains(p.Key)) 61 | .Select(p => $"{p.Key}: \"{p.Value}\"") 62 | .ToArray(); 63 | if (properties.Any()) 64 | { 65 | code.AppendLine($"{getName(obj.Name)} = Table({{ Name:\"{obj.Name}\", Type:\"{type}\", {string.Join(", ", properties)} }})"); 66 | } 67 | else 68 | { 69 | code.AppendLine($"{getName(obj.Name)} = Table({{ Name:\"{obj.Name}\", Type:\"{type}\" }})"); 70 | } 71 | if (obj.Data.TryGetValue("OnSelect", out var onSelect)) 72 | { 73 | var val = onSelect.TrimStart('='); 74 | code.AppendLine($"OnSelect = {val}"); 75 | } 76 | else if (obj.Data.TryGetValue("Text", out var text)) 77 | { 78 | var val = text.TrimStart('='); 79 | code.AppendLine($"Text = {val}"); 80 | code.AppendLine(); 81 | } 82 | 83 | return code.ToString(); 84 | } 85 | 86 | private string getName(string value) 87 | { 88 | return value.TrimStart('/').Replace("/", "_"); 89 | } 90 | 91 | public string GetObjectPreview(PowerAppComponent component, PowerAppComponentObject obj) 92 | { 93 | var html = new StringBuilder(); 94 | var type = component.Types.TryGetValue(obj.Name, out var t) ? t : "Cell"; 95 | html.Append("
$"top:{getNumber(value)}px;", 108 | "Y" => $"left:{getNumber(value)}px;", 109 | "Height" => $"height:{getNumber(value)}px;", 110 | "Width" => $"width:{getNumber(value)}px;", 111 | "ZIndex" => $"z-index:{getNumber(value)};", 112 | "Size" => $"font-size:{getNumber(value)}px;", 113 | "Text" => $"content:'{getText(value)}';", 114 | "Fill" => $"background-color:{getColor(value)};", 115 | "Color" => $"color:{getColor(value)};", 116 | "BorderColor" => $"border-color:{getColor(value)};", 117 | "BorderThickness" => $"border-width:{getNumber(value)}px;", 118 | _ => "" 119 | }); 120 | } 121 | } 122 | html.AppendLine("\">"); 123 | html.AppendLine($"

{getName(obj.Name)} ({type})

"); 124 | 125 | if (obj.Data.TryGetValue("Text", out var text)) 126 | { 127 | var val = text.TrimStart('='); 128 | html.AppendLine(val); 129 | } 130 | 131 | html.AppendLine("
"); 132 | return html.ToString(); 133 | } 134 | 135 | private string getColor(string value) 136 | { 137 | if (value.StartsWith("RGBA(")) 138 | { 139 | return value.Replace("RGBA", "rgba"); 140 | } 141 | else 142 | { 143 | return value; 144 | } 145 | } 146 | 147 | private string getNumber(string value) 148 | { 149 | if (System.Text.RegularExpressions.Regex.IsMatch(value, "\\D")) 150 | { 151 | return "00"; 152 | } 153 | else 154 | { 155 | return value; 156 | } 157 | } 158 | 159 | private string getText(string value) 160 | { 161 | if (value.Contains('(')) 162 | { 163 | return ""; 164 | } 165 | else 166 | { 167 | return value.Replace("\"", ""); 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_h.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *_wpftmp.csproj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush personal settings 296 | .cr/personal 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | # Local History for Visual Studio 334 | .localhistory/ 335 | -------------------------------------------------------------------------------- /src/Services/FileManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Forms; 2 | using System.IO.Compression; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace PowerNote.Services; 6 | 7 | public class FileManager 8 | { 9 | private const string Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 10 | private const string AppsFolderName = "Apps"; 11 | private DirectoryInfo AppsFolder => Directory.CreateDirectory(AppsFolderName); 12 | private string AppFilePath(string name) => Path.Combine(AppsFolder.FullName, name); 13 | private string ColumnLetter(int column) => 14 | column < 1 15 | ? "A" 16 | : 17 | column < Letters.Length 18 | ? Letters[column - 1].ToString() 19 | : string.Concat(Enumerable.Repeat(Letters[column - 1].ToString(), column / Letters.Length)); 20 | 21 | private Dictionary _powerApps = new(); 22 | private string _codeFromFile; 23 | private readonly AppReader _appReader; 24 | private readonly ExcelReader _excelReader; 25 | 26 | public FileManager(AppReader appReader, ExcelReader excelReader) 27 | { 28 | _appReader = appReader; 29 | _excelReader = excelReader; 30 | } 31 | 32 | public async ValueTask GetPowerAppFileAsync(string name) => await File.ReadAllBytesAsync(_powerApps[name].FilePath); 33 | 34 | public PowerAppComponent[] GetPowerAppComponents(string name) => _powerApps[name].Components; 35 | 36 | public PowerApp[] GetPowerApps() => _powerApps.Values.ToArray(); 37 | public string GetLoadedCode() => _codeFromFile; 38 | 39 | public void WriteCode(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".pfx"), bytes); 40 | 41 | public void WriteApp(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".msapp"), bytes); 42 | 43 | public void WriteExcel(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".xlsx"), bytes); 44 | 45 | public void WriteFlow(string name, byte[] bytes) => File.WriteAllBytes(AppFilePath(name + ".json"), bytes); 46 | 47 | public string GetAppUrl(PowerApp app) 48 | { 49 | var ext = Path.GetExtension(app.FilePath).TrimStart('.').ToLower(); 50 | return UrlManager.CreateUrl(File.ReadAllBytes(app.FilePath), ext); 51 | } 52 | 53 | public void ReadAppsFolder() 54 | { 55 | var dir = AppsFolder; 56 | var files = Directory.EnumerateFiles(dir.FullName, "*.*", SearchOption.AllDirectories).ToArray(); 57 | foreach (var file in files) 58 | { 59 | var name = Path.GetFileNameWithoutExtension(file); 60 | var ext = Path.GetExtension(file).TrimStart('.'); 61 | try 62 | { 63 | if (ext.Equals("msapp", StringComparison.InvariantCultureIgnoreCase)) 64 | { 65 | if (!_powerApps.TryGetValue(name, out var app)) 66 | { 67 | _powerApps[name] = _appReader.ReadApp(file); 68 | } 69 | else 70 | { 71 | Console.WriteLine($"ReadApp ({name}): Already loaded"); 72 | } 73 | } 74 | else if (ext.Equals("zip", StringComparison.InvariantCultureIgnoreCase)) 75 | { 76 | SaveAppPackage(File.ReadAllBytes(file)); 77 | } 78 | else if (ext.Equals("xlsx", StringComparison.InvariantCultureIgnoreCase)) 79 | { 80 | var components = ReadExcelFile(file); 81 | _powerApps[name] = new(name, components, file); 82 | } 83 | else if (ext.Equals("json", StringComparison.InvariantCultureIgnoreCase)) 84 | { 85 | _powerApps[name] = ReadFlow(file); 86 | } 87 | else if (ext.Equals("pfx", StringComparison.InvariantCultureIgnoreCase)) 88 | { 89 | _codeFromFile = File.ReadAllText(file); 90 | } 91 | else 92 | { 93 | Console.WriteLine($"Removing unnecessary file: {file}"); 94 | File.Delete(file); 95 | } 96 | } 97 | catch (Exception ex) 98 | { 99 | Console.WriteLine($"ReadApp Error ({name}): {ex.ToString()}"); 100 | } 101 | } 102 | } 103 | 104 | public void SaveAppPackage(byte[] zipFile) 105 | { 106 | var apps = WriteAppPackage(zipFile); 107 | foreach(var app in apps) 108 | { 109 | _powerApps[app.Name] = app; 110 | } 111 | } 112 | 113 | private PowerAppComponent[] ReadExcelFile(string xlsxFilePath) 114 | { 115 | var components = new List(); 116 | var cells = _excelReader.ReadExcel(xlsxFilePath); 117 | var sheets = cells.GroupBy(c => c.Sheet).ToArray(); 118 | foreach (var sheet in sheets) 119 | { 120 | var rows = sheet.GroupBy(c => c.Row).ToArray(); 121 | var types = new Dictionary(); 122 | var objects = new Dictionary>(); 123 | foreach (var row in rows) 124 | { 125 | foreach (var column in row) 126 | { 127 | var cell = column.Formula ?? column.Text; 128 | if (!string.IsNullOrWhiteSpace(cell)) 129 | { 130 | objects[$"{ColumnLetter(column.Column)}{column.Row}"] = new() 131 | { 132 | { "Text", cell }, 133 | { "X", ((column.Column - 1) * 120).ToString() }, 134 | { "Y", ((column.Row - 1) * 20).ToString() }, 135 | { "Width", "120" }, 136 | { "Height", "120" } 137 | }; 138 | } 139 | } 140 | } 141 | components.Add(new(sheet.Key, types, objects)); 142 | } 143 | return components.ToArray(); 144 | } 145 | 146 | public PowerApp ReadFlow(string jsonFilePath) 147 | { 148 | var name = Path.GetFileNameWithoutExtension(jsonFilePath); 149 | var json = File.ReadAllText(jsonFilePath); 150 | var formulas = Regex.Matches(json, @"""@\{?.*\}?""").Select(m => m.Value).ToArray(); 151 | var component = new PowerAppComponent("Formulas", new(), formulas.Distinct().ToDictionary(f => f, formula => new Dictionary() 152 | { 153 | { "Text", formula } 154 | })); 155 | return new(name, new[] { component }, jsonFilePath); 156 | } 157 | 158 | public async ValueTask UploadFilesAsync(IEnumerable files) 159 | { 160 | foreach (var item in files) 161 | { 162 | var path = AppFilePath(item.Name); 163 | using var stream = item.OpenReadStream(); 164 | using var file = File.Create(path); 165 | await stream.CopyToAsync(file); 166 | } 167 | } 168 | 169 | private IEnumerable WriteAppPackage(byte[] zipFile) 170 | { 171 | using var stream = new MemoryStream(zipFile); 172 | using var zip = new ZipArchive(stream, ZipArchiveMode.Read); 173 | foreach (var entry in zip.Entries) 174 | { 175 | if (entry.Name.EndsWith(".msapp", StringComparison.InvariantCultureIgnoreCase)) 176 | { 177 | var path = AppFilePath(entry.Name); 178 | entry.ExtractToFile(path); 179 | yield return _appReader.ReadApp(path); 180 | } 181 | else if (entry.Name.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase) 182 | && entry.FullName.Contains("workflows", StringComparison.InvariantCultureIgnoreCase)) 183 | { 184 | var path = AppFilePath(entry.Name); 185 | entry.ExtractToFile(path); 186 | yield return ReadFlow(path); 187 | } 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /src/Services/ExcelReader.cs: -------------------------------------------------------------------------------- 1 | using PowerNote.Models; 2 | using System.IO.Compression; 3 | using System.Text.RegularExpressions; 4 | using System.Xml; 5 | using System.Xml.Serialization; 6 | 7 | namespace PowerNote.Services; 8 | 9 | public record SpreadsheetCellReference(string Sheet, int Row, int Column); 10 | public record SpreadsheetCellRange(string FromSheet, int FromRow, int FromColumn, int ToRow, int ToColumn) : SpreadsheetCellReference(FromSheet, FromRow, FromColumn); 11 | public record SpreadsheetCell(string CellSheet, int CellRow, int CellColumn, string Text, string Formula) : SpreadsheetCellReference(CellSheet, CellRow, CellColumn) 12 | { 13 | public SpreadsheetCellReference MergedToCell { get; set; } 14 | } 15 | 16 | public class ExcelReader 17 | { 18 | public IEnumerable ReadExcel(string xlsxPath) 19 | { 20 | var items = new List(); 21 | using var file = File.OpenRead(xlsxPath); 22 | using var zipArchive = new ZipArchive(file, ZipArchiveMode.Read, false); 23 | var workbook = ObjectZipEntry(GetZipArchiveEntry(zipArchive, "xl/workbook.xml")); 24 | var stringTable = ObjectZipEntry(GetZipArchiveEntry(zipArchive, "xl/sharedStrings.xml"))?.Items; 25 | var relationships = XmlDocumentZipEntry(GetZipArchiveEntry(zipArchive, "xl/_rels/workbook.xml.rels")); 26 | foreach (var sheet in workbook.Sheets) 27 | { 28 | if (!string.IsNullOrEmpty(sheet.SheetReferenceId)) 29 | { 30 | var sheetPath = FindRelationshipTarget(sheet.SheetReferenceId, relationships); 31 | if (!string.IsNullOrEmpty(sheetPath)) 32 | { 33 | var worksheet = ObjectZipEntry(GetZipArchiveEntry(zipArchive, "xl/" + sheetPath)); 34 | if (worksheet.Rows != null) 35 | { 36 | var merged_items = new List(); 37 | if (worksheet.MergedCells != null && worksheet.MergedCells.Length > 0) 38 | { 39 | merged_items.AddRange(worksheet.MergedCells.Where(m => m.CellRange.Contains(":")).Select(m => 40 | { 41 | var cellRange = GetCellReference(m.CellRange, sheet.Name) as SpreadsheetCellRange; 42 | if (cellRange != null) 43 | { 44 | return cellRange; 45 | } 46 | else 47 | { 48 | return null; 49 | } 50 | }).Where(r => r != null)); 51 | } 52 | var sheetIndex = workbook.Sheets.ToList().IndexOf(sheet); 53 | items.AddRange(from row in worksheet.Rows 54 | where row.Cells != null 55 | from cell in row.Cells 56 | let t = GetText(stringTable, cell) 57 | where !string.IsNullOrEmpty(t) || !string.IsNullOrEmpty(cell.Formula) 58 | let s = sheet.Name 59 | let r = row.RowReference - 1 60 | let c = GetColumnIndex(cell.CellReference) 61 | let m = merged_items.Where(m => m.Sheet == s && m.Row == r && m.Column == c).FirstOrDefault() 62 | select new SpreadsheetCell(s, r, c, t, cell.Formula) 63 | { 64 | MergedToCell = (m != null ? new SpreadsheetCellReference(m.Sheet, m.Row, m.Column) : null) 65 | }); 66 | } 67 | } 68 | } 69 | } 70 | return items; 71 | } 72 | 73 | private static string GetText(ExcelWorkbookStringTableText[] stringTableItems, ExcelWorksheetCell cell) 74 | { 75 | int s = 0; 76 | if (cell.CellType == "s" && int.TryParse(cell.Value, out s) && s < stringTableItems.Length) 77 | { 78 | return stringTableItems[s].Text?.Trim(); 79 | } 80 | else if (cell.CellType == "str") 81 | { 82 | if (!string.IsNullOrEmpty(cell.Value)) 83 | { 84 | return cell.Value.Trim(); 85 | } 86 | else 87 | { 88 | return string.Empty; 89 | } 90 | } 91 | else if (!string.IsNullOrEmpty(cell.Value)) 92 | { 93 | //double amount; 94 | //if (double.TryParse(cell.Value, out amount)) 95 | //{ 96 | // var date = DateTime.FromOADate(amount); 97 | // return date.ToShortDateString(); 98 | // //return amount.ToString("#,##0.##"); 99 | //} 100 | //else 101 | //{ 102 | // return cell.Value; 103 | //} 104 | return cell.Value; 105 | } 106 | else 107 | { 108 | return string.Empty; 109 | } 110 | } 111 | 112 | private static int GetColumnIndex(string cellReference) 113 | { 114 | var colLetters = new Regex("[A-Za-z]+").Match(cellReference).Value.ToUpper(); 115 | var colIndex = 0; 116 | for (int i = 0; i < colLetters.Length; i++) 117 | { 118 | colIndex *= 26; 119 | colIndex += (colLetters[i] - 'A' + 1); 120 | } 121 | return colIndex - 1; 122 | } 123 | 124 | private static int GetRowIndex(string cellReference) 125 | { 126 | var cellNumbers = new Regex("[0-9]+").Match(cellReference).Value; 127 | if (!string.IsNullOrEmpty(cellNumbers)) 128 | { 129 | return Convert.ToInt32(cellNumbers) - 1; 130 | } 131 | else 132 | { 133 | return -1; 134 | } 135 | } 136 | 137 | private static SpreadsheetCellReference GetCellReference(string cellReference, string currentSheet) 138 | { 139 | if (cellReference.Contains(':')) 140 | { 141 | var cellFrom = GetCellReference(cellReference.Split(':')[0], currentSheet); 142 | var cellTo = GetCellReference(cellReference.Split(':')[1], currentSheet); 143 | if (cellFrom.Row > cellTo.Row || cellFrom.Column > cellTo.Column) 144 | { 145 | return null; 146 | } 147 | else 148 | { 149 | return new SpreadsheetCellRange(cellFrom.Sheet, cellFrom.Row, cellFrom.Column, cellTo.Row, cellTo.Column); 150 | } 151 | } 152 | else if (cellReference.Contains("#REF!")) 153 | { 154 | return null; 155 | } 156 | else if (cellReference.Contains('!')) 157 | { 158 | var sheet = cellReference.Split('!')[0].Replace("'", ""); 159 | var cell = cellReference.Split('!')[1]; 160 | var row = GetRowIndex(cell); 161 | var column = GetColumnIndex(cell); 162 | return new SpreadsheetCellReference(sheet, row, column); 163 | } 164 | else 165 | { 166 | var row = GetRowIndex(cellReference); 167 | var column = GetColumnIndex(cellReference); 168 | return new SpreadsheetCellReference(currentSheet, row, column); 169 | } 170 | } 171 | 172 | private static string FindRelationshipTarget(string relId, XmlDocument relationships) 173 | { 174 | var sheetReference = relationships.SelectSingleNode("//node()[@Id='" + relId + "']"); 175 | if (sheetReference != null) 176 | { 177 | var targetAttribute = sheetReference.Attributes["Target"]; 178 | if (targetAttribute != null) 179 | { 180 | return targetAttribute.Value; 181 | } 182 | } 183 | return null; 184 | } 185 | 186 | private static ZipArchiveEntry GetZipArchiveEntry(ZipArchive zipArchive, string zipPath) 187 | { 188 | return zipArchive.Entries.First(n => n.FullName.Equals(zipPath)); 189 | } 190 | 191 | private static T ObjectZipEntry(ZipArchiveEntry zipArchiveEntry) 192 | { 193 | using (var stream = zipArchiveEntry.Open()) 194 | return (T)new XmlSerializer(typeof(T)).Deserialize(XmlReader.Create(stream)); 195 | } 196 | 197 | private static XmlDocument XmlDocumentZipEntry(ZipArchiveEntry zipArchiveEntry) 198 | { 199 | var xmlDocument = new XmlDocument(); 200 | using (var stream = zipArchiveEntry.Open()) 201 | { 202 | xmlDocument.Load(stream); 203 | return xmlDocument; 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/wwwroot/scripts/MonacoEditor.js: -------------------------------------------------------------------------------- 1 | function createHandler(text) { 2 | return new Function("value", `return ${text}`); 3 | } 4 | var _updateMonacoEditor; 5 | export function updateMonacoEditor(code) { 6 | if (typeof (_updateMonacoEditor) === 'function') { 7 | _updateMonacoEditor(code); 8 | } 9 | } 10 | export function loadMonacoEditor(id, code, onChangeHandler, onHoverHandler, onSuggestHandler, onSaveHandler, onExecuteHandler, onPreviewHandler) { 11 | const onChange = createHandler(onChangeHandler); 12 | const onHover = createHandler(onHoverHandler); 13 | const onSuggest = createHandler(onSuggestHandler); 14 | const onSave = createHandler(onSaveHandler); 15 | const onExecute = createHandler(onExecuteHandler); 16 | const onPreview = createHandler(onPreviewHandler); 17 | const container = document.getElementById(id); 18 | const language = "PowerFx"; 19 | const languageExt = "pfx"; 20 | const theme = "vs-dark"; 21 | const loaderScript = document.createElement("script"); 22 | loaderScript.src = "https://www.typescriptlang.org/js/vs.loader.js"; 23 | loaderScript.async = true; 24 | loaderScript.onload = () => { 25 | require.config({ 26 | paths: { 27 | vs: "https://typescript.azureedge.net/cdn/4.0.5/monaco/min/vs", 28 | sandbox: "https://www.typescriptlang.org/js/sandbox" 29 | }, 30 | ignoreDuplicateModules: ["vs/editor/editor.main"] 31 | }); 32 | require(["vs/editor/editor.main", "sandbox/index"], async (editorMain, sandboxFactory) => { 33 | monaco.languages.register({ 34 | id: language, 35 | aliases: [languageExt], 36 | extensions: [languageExt] 37 | }); 38 | const model = monaco.editor.createModel(code, language, monaco.Uri.parse(`file:///index.pfx`)); 39 | model.setValue(code); 40 | addMonacoEditorSuggestions(monaco, language, (text, column) => { 41 | const items = []; 42 | const suggested = onSuggest(text); 43 | for (let suggest of suggested) { 44 | items.push({ 45 | startColumn: column, 46 | text: suggest.text, 47 | description: suggest.description 48 | }); 49 | } 50 | return items; 51 | }); 52 | addMonacoEditorHover(monaco, language, (text, column) => onHover(text)); 53 | const editor = monaco.editor.create(container, { 54 | model, 55 | language, 56 | theme, 57 | ...defaultMonacoEditorOptions(), 58 | lineNumbers: (lineNumber) => { 59 | const lines = model.getLinesContent(); 60 | var line = 0; 61 | for (var i = 0; i < lines.length && i + 1 < lineNumber; i++) { 62 | if (lines[i] && lines[i].trim()) { 63 | line += 1; 64 | } 65 | } 66 | const content = model.getLineContent(lineNumber); 67 | if (content && content.trim()) { 68 | return (line + 1).toString(); 69 | } 70 | else { 71 | return ""; 72 | } 73 | } 74 | }); 75 | _updateMonacoEditor = (code) => editor.setValue(code); 76 | monaco.languages.registerCodeLensProvider(language, new MonacoCodeLensProvider(editor, onChange, onExecute, onPreview)); 77 | editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => onSave(editor.getValue())); 78 | window.addEventListener("resize", () => editor.layout()); 79 | }); 80 | }; 81 | document.body.appendChild(loaderScript); 82 | } 83 | function addMonacoEditorHover(monaco, language, onHover) { 84 | monaco.languages.registerHoverProvider(language, { 85 | provideHover: async (model, position) => { 86 | const line = model.getLineContent(position.lineNumber); 87 | const hover = await onHover(line, position.column); 88 | if (hover) { 89 | return { 90 | contents: [{ value: `## ${hover.text}\n${hover.description}` }], 91 | range: { 92 | startLineNumber: position.lineNumber, 93 | endLineNumber: position.lineNumber, 94 | startColumn: hover.startColumn || 1, 95 | endColumn: hover.endColumn || line.length 96 | } 97 | }; 98 | } 99 | } 100 | }); 101 | } 102 | function addMonacoEditorSuggestions(monaco, language, onSuggest) { 103 | monaco.languages.registerCompletionItemProvider(language, { 104 | provideCompletionItems: async (model, position) => { 105 | const textUntilPosition = model.getValueInRange({ 106 | startLineNumber: position.lineNumber, 107 | startColumn: 1, 108 | endLineNumber: position.lineNumber, 109 | endColumn: position.column 110 | }); 111 | const lineSuggestions = await onSuggest(textUntilPosition, position.column); 112 | if (lineSuggestions && lineSuggestions.length > 0) { 113 | const word = model.getWordUntilPosition(position); 114 | const suggestions = []; 115 | for (const suggestion of lineSuggestions) { 116 | suggestions.push({ 117 | label: suggestion.text, 118 | detail: suggestion.description, 119 | kind: monaco.languages.CompletionItemKind.Value, 120 | insertText: suggestion.text, 121 | range: { 122 | startLineNumber: position.lineNumber, 123 | endLineNumber: position.lineNumber, 124 | startColumn: suggestion.startColumn || word.startColumn, 125 | endColumn: suggestion.endColumn || word.endColumn 126 | } 127 | }); 128 | } 129 | return { suggestions }; 130 | } 131 | } 132 | }); 133 | } 134 | function defaultMonacoEditorOptions() { 135 | return { 136 | lineHeight: 30, 137 | fontSize: 22, 138 | renderLineHighlight: 'all', 139 | wordWrap: 'on', 140 | scrollBeyondLastLine: false, 141 | minimap: { 142 | enabled: false 143 | }, 144 | renderValidationDecorations: 'off', 145 | lineDecorationsWidth: 0, 146 | glyphMargin: false, 147 | contextmenu: false, 148 | codeLens: true, 149 | mouseWheelZoom: true, 150 | quickSuggestions: false, 151 | suggest: { 152 | showIssues: false, 153 | shareSuggestSelections: false, 154 | showIcons: false, 155 | showMethods: false, 156 | showFunctions: false, 157 | showVariables: false, 158 | showKeywords: false, 159 | showWords: false, 160 | showClasses: false, 161 | showColors: false, 162 | showConstants: false, 163 | showConstructors: false, 164 | showEnumMembers: false, 165 | showEnums: false, 166 | showEvents: false, 167 | showFields: false, 168 | showFiles: false, 169 | showFolders: false, 170 | showInterfaces: false, 171 | showModules: false, 172 | showOperators: false, 173 | showProperties: false, 174 | showReferences: false, 175 | showSnippets: false, 176 | showStructs: false, 177 | showTypeParameters: false, 178 | showUnits: false, 179 | showValues: true, 180 | filterGraceful: false 181 | } 182 | }; 183 | } 184 | class MonacoCodeLensProvider { 185 | constructor(editor, check, execute, preview) { 186 | this.editor = editor; 187 | this.check = check; 188 | this.execute = execute; 189 | this._errorCommand = editor.addCommand(0, function (_, result) { 190 | preview("Error: " + result.text); 191 | }, ''); 192 | this._previewCommand = editor.addCommand(1, function (_, result) { 193 | console.log('_previewCommand', arguments); 194 | preview(result.text?.trim()); 195 | }, ''); 196 | } 197 | resolveCodeLens(model, codeLens) { 198 | return codeLens; 199 | } 200 | async provideCodeLenses(model) { 201 | const lines = model.getLinesContent(); 202 | const decorations = []; 203 | const lenses = []; 204 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 205 | const lineNum = lineIndex + 1; 206 | const expr = lines[lineIndex]; 207 | if (expr) { 208 | const result = await this.check(expr); 209 | console.log("check", { ...result }); 210 | if (result.errors) { 211 | const errors = []; 212 | for (var err of result.errors) { 213 | console.log("error", err.description); 214 | decorations.push({ 215 | id: `error_${lineNum}_${err.start}_${err.end}`, 216 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1), 217 | options: { 218 | inlineClassName: "powerfx-error", 219 | hoverMessage: { value: err.description } 220 | } 221 | }); 222 | errors.push(err.description); 223 | } 224 | result.text = errors.join('\n'); 225 | lenses.push({ 226 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1), 227 | id: `preview_${lineNum}_${err.start}_${err.end}`, 228 | command: { 229 | id: this._errorCommand, 230 | title: "Error(s)", 231 | tooltip: result.text, 232 | arguments: [result] 233 | } 234 | }); 235 | } 236 | else if (result.type !== "BlankType") { 237 | const output = await this.execute(expr); 238 | console.log("execute", { ...output }); 239 | decorations.push({ 240 | id: `type_${lineNum}`, 241 | range: new monaco.Range(lineNum, 1, lineNum, 1), 242 | options: { 243 | inlineClassName: "powerfx-type", 244 | hoverMessage: { value: result.type } 245 | } 246 | }); 247 | lenses.push({ 248 | range: new monaco.Range(lineNum, 1, lineNum, 1), 249 | id: `preview_${lineNum}`, 250 | command: { 251 | id: this._previewCommand, 252 | title: "View Result", 253 | tooltip: result.text, 254 | arguments: [output] 255 | } 256 | }); 257 | } 258 | } 259 | } 260 | this._decorations = this.editor.deltaDecorations(this._decorations || [], decorations); 261 | return { 262 | lenses, 263 | dispose: () => { } 264 | }; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @inject IJSInProcessRuntime JSRuntime 3 | @inject PowerFxHost Host 4 | @inject FileManager Files 5 | @inject UrlManager Url 6 | @inject AppPreviewer AppPreview 7 | @using System.Text 8 | 9 | PowerNote 10 | 11 | 12 | 13 |
14 | 15 |
16 |

17 | 18 | PowerNote 19 |

20 |
21 | 22 |
23 |
24 |
Click here to add files
25 |
26 | Power Apps 27 | Canvas Apps (.msapp) 28 |
29 |
30 | Power Automate 31 | Cloud Flows (.json) 32 |
33 |
34 | Excel 35 | Excel Documents (.xlsx) 36 |
37 |
38 | Dataverse 39 | Dataverse Solutions (.zip) 40 |
41 | @if (!ShowApps) 42 | { 43 |
44 | 45 | Or press ESC to use the Power Fx editor without a file 46 | 47 |
48 | } 49 |
50 | Note: Files must be smaller than 512kb at this time.
51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 |
59 | 66 |
67 | 68 | @if (ShowApps) 69 | { 70 | 114 | } 115 |
116 | 117 | @if (ShowPreview) 118 | { 119 | 120 | } 121 | 122 |
123 | 124 |
125 | 126 | @code { 127 | private static PowerFxHost _host; 128 | private static Action _showEditor; 129 | private static Action _onSaveCode; 130 | private static Action _showPreview; 131 | 132 | [JSInvokable("SaveMonacoEditorValue")] 133 | public static void SaveCode(string code) 134 | { 135 | _onSaveCode(code); 136 | } 137 | 138 | [JSInvokable("ExecutePowerFxCode")] 139 | public static object ExecuteCode(string code) 140 | { 141 | var result = _host.Execute(code); 142 | return new 143 | { 144 | code = code.Trim(), 145 | name = result.Result.Name, 146 | text = result.Output, 147 | type = result.Result.Value?.Type.GetType().Name ?? "Error" 148 | }; 149 | } 150 | 151 | [JSInvokable("CheckPowerFxCode")] 152 | public static object CheckCode(string code) 153 | { 154 | var check = _host.Check(code); 155 | return new 156 | { 157 | code = code.Trim(), 158 | name = check.Name, 159 | text = check.Description, 160 | type = check.Type?.GetType().Name ?? "Error", 161 | errors = check.Errors?.Select(e => new 162 | { 163 | text = e.Text, 164 | description = e.Description, 165 | start = e.Start, 166 | end = e.End 167 | }).ToArray() 168 | }; 169 | } 170 | 171 | [JSInvokable("ShowPowerFxCode")] 172 | public static object ShowCode(string code) 173 | { 174 | var suggestion = _host.Show(code); 175 | if (suggestion != null) 176 | { 177 | return new 178 | { 179 | text = suggestion.Text, 180 | description = suggestion.Description, 181 | startColumn = suggestion.Start, 182 | endColumn = suggestion.End 183 | }; 184 | } 185 | else 186 | { 187 | return null; 188 | } 189 | } 190 | 191 | [JSInvokable("SuggestPowerFxCode")] 192 | public static object[] SuggestCode(string code) 193 | { 194 | return _host.Suggest(code)?.Select(s => new 195 | { 196 | text = s.Text, 197 | description = s.Description, 198 | startColumn = s.Start, 199 | endColumn = s.End 200 | }).ToArray(); 201 | } 202 | 203 | [JSInvokable("ShowPowerFxEditor")] 204 | public static void ShowEditor() 205 | { 206 | _showEditor(); 207 | } 208 | 209 | [JSInvokable("PreviewPowerFxCode")] 210 | public static void PreviewCode(string output) 211 | { 212 | _showPreview(output); 213 | } 214 | 215 | public PowerApp[] Apps; 216 | public PowerAppComponent[] Components; 217 | public PowerAppComponentObject[] Objects; 218 | public PowerApp SelectedApp; 219 | public PowerAppComponent SelectedAppComponent; 220 | public PowerAppComponentObject SelectedObject; 221 | 222 | private string Code; 223 | private string Output; 224 | private bool ShowApps; 225 | private bool ShowPreview; 226 | private string FileDropStyle; 227 | 228 | public void showEditor() 229 | { 230 | if (Apps == null) 231 | { 232 | Code = PowerFxSample.Code; 233 | FileDropStyle = "files top-right"; 234 | StateHasChanged(); 235 | } 236 | } 237 | 238 | public void OnFilesAdded() 239 | { 240 | Files.ReadAppsFolder(); 241 | Apps = Files.GetPowerApps(); 242 | ShowApps = Apps.Any(); 243 | if (Apps.Length == 1) 244 | { 245 | SelectedApp = Apps.First(); 246 | Components = SelectedApp.Components; 247 | } 248 | FileDropStyle = "files top-right"; 249 | StateHasChanged(); 250 | } 251 | 252 | private void onSaveCode(string code) 253 | { 254 | var bytes = System.Text.Encoding.UTF8.GetBytes(code); 255 | Url.SetUrl(UrlManager.CreateUrl(bytes)); 256 | } 257 | 258 | private void onSelectApp(PowerApp app) 259 | { 260 | SelectedApp = app; 261 | Components = app.Components; 262 | StateHasChanged(); 263 | Url.SetUrl(Files.GetAppUrl(app)); 264 | } 265 | 266 | private void onSelectAppComponent(PowerAppComponent component) 267 | { 268 | SelectedAppComponent = component; 269 | Objects = component.Objects; 270 | StateHasChanged(); 271 | } 272 | 273 | private void onSelectObject(PowerAppComponentObject obj) 274 | { 275 | SelectedObject = obj; 276 | Code = AppPreview.GetObjectTable(SelectedAppComponent, obj); 277 | StateHasChanged(); 278 | } 279 | 280 | private void showComponentPreview() 281 | { 282 | var preview = AppPreview.GetAppComponentPreview(SelectedApp, SelectedAppComponent); 283 | Code = preview.Code; 284 | Output = preview.Html; 285 | ShowPreview = true; 286 | StateHasChanged(); 287 | } 288 | 289 | private void showObjectPreview() 290 | { 291 | if (SelectedObject != null) 292 | { 293 | Output = AppPreview.GetObjectPreview(SelectedAppComponent, SelectedObject); 294 | ShowPreview = true; 295 | StateHasChanged(); 296 | } 297 | } 298 | 299 | public void showPreview(string output) 300 | { 301 | Console.WriteLine("showPreview: " + output); 302 | Output = output; 303 | ShowPreview = true; 304 | StateHasChanged(); 305 | } 306 | 307 | private void hideObjectPreview() 308 | { 309 | ShowPreview = false; 310 | StateHasChanged(); 311 | } 312 | 313 | private void clearSelectedApp() 314 | { 315 | SelectedApp = null; 316 | SelectedAppComponent = null; 317 | Components = null; 318 | Objects = null; 319 | StateHasChanged(); 320 | } 321 | 322 | private void clearSelectedComponent() 323 | { 324 | SelectedAppComponent = null; 325 | Objects = null; 326 | StateHasChanged(); 327 | } 328 | 329 | protected override void OnInitialized() 330 | { 331 | _host = Host; 332 | _showEditor = showEditor; 333 | _showPreview = showPreview; 334 | _onSaveCode = onSaveCode; 335 | 336 | FileDropStyle = "files full-screen"; 337 | 338 | Url.ReadUrl(OnFilesAdded); 339 | 340 | Code = Files.GetLoadedCode(); 341 | 342 | JSRuntime.InvokeVoid("eval", @"window.onkeyup = function(e) { 343 | if (e.key === 'Escape') { 344 | DotNet.invokeMethod('PowerNote', 'ShowPowerFxEditor') 345 | window.onkeyup = null 346 | } 347 | }"); 348 | } 349 | } -------------------------------------------------------------------------------- /src/Services/PowerFxHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.PowerFx; 2 | using Microsoft.PowerFx.Core.Errors; 3 | using Microsoft.PowerFx.Core.Public; 4 | using Microsoft.PowerFx.Core.Public.Types; 5 | using Microsoft.PowerFx.Core.Public.Values; 6 | using PowerNote.Models; 7 | using System.Diagnostics; 8 | using System.Text; 9 | using System.Text.RegularExpressions; 10 | 11 | namespace PowerNote.Services; 12 | 13 | public record PowerFxError(int Begin, int End, ErrorKind Kind, DocumentErrorSeverity Severity, string Message, string Name, FormulaValue Value) : PowerFxResult(Name, Message, Value); 14 | 15 | public record PowerFxResult(string Name, string FormattedText, FormulaValue Value); 16 | public record PowerFxCheck(string Name, string Description, FormulaType Type, PowerFxSuggestion[] Errors); 17 | public record PowerFxSuggestion(string Text, string Description, int Start, int End); 18 | 19 | public class PowerFxHost 20 | { 21 | private RecalcEngine _engine; 22 | private readonly FormulaJsHost _formulaJsHost; 23 | 24 | private readonly HashSet _formulas = new(); 25 | private readonly HashSet _variables = new(); 26 | 27 | public PowerFxHost(FormulaJsHost formulaJsHost) 28 | { 29 | _formulaJsHost = formulaJsHost; 30 | _engine = new(); 31 | _formulaJsHost.AddFunctions(_engine); 32 | } 33 | 34 | public PowerFxSuggestion Show(string expr) 35 | { 36 | Console.WriteLine($"Show: {expr}"); 37 | var functions = _engine.GetAllFunctionNames().ToArray(); 38 | foreach (var function in functions) 39 | { 40 | var functionIndex = expr.IndexOf($"{function}("); 41 | if (functionIndex > -1 && PowerFxGrammar.FunctionDescriptions.ContainsKey(function)) 42 | { 43 | return new(function, PowerFxGrammar.FunctionDescriptions[function], functionIndex, functionIndex + function.Length); 44 | } 45 | } 46 | return null; 47 | } 48 | 49 | public IEnumerable Suggest(string expr) 50 | { 51 | Console.WriteLine($"Suggest: {expr}"); 52 | var result = _engine.Suggest(expr, null, expr.Length); 53 | foreach (var suggestion in result.Suggestions) 54 | { 55 | if (suggestion.Overloads.Any()) 56 | { 57 | foreach(var overload in suggestion.Overloads) 58 | { 59 | yield return new(overload.DisplayText.Text, overload.Definition, overload.DisplayText.HighlightStart, overload.DisplayText.HighlightEnd); 60 | } 61 | } 62 | else 63 | { 64 | yield return new(suggestion.DisplayText.Text, suggestion.Definition, suggestion.DisplayText.HighlightStart, suggestion.DisplayText.HighlightEnd); 65 | } 66 | } 67 | var functions = _engine.GetAllFunctionNames().ToArray(); 68 | foreach (var function in functions) 69 | { 70 | if (string.IsNullOrEmpty(expr)) 71 | { 72 | yield return new(function, PowerFxGrammar.FunctionDescriptions[function], 1, 1); 73 | } 74 | else 75 | { 76 | var functionIndex = expr.IndexOf($"{function}("); 77 | if (functionIndex > -1 && PowerFxGrammar.FunctionDescriptions.ContainsKey(function)) 78 | { 79 | yield return new(function, PowerFxGrammar.FunctionDescriptions[function], functionIndex, functionIndex + function.Length); 80 | } 81 | } 82 | } 83 | } 84 | 85 | public PowerFxCheck Check(string expr) 86 | { 87 | Debug.WriteLine($"Check: {expr}"); 88 | try 89 | { 90 | var result = checkExpression(expr); 91 | if (result.check != null) { 92 | if (result.check.IsSuccess) 93 | { 94 | if (result.isAssignment) 95 | { 96 | return new(result.name, "Assignment", result.check.ReturnType, null); 97 | } 98 | else if (result.isFormula) 99 | { 100 | if (!_formulas.Contains(result.name)) 101 | { 102 | _engine.SetFormula(result.name, result.text, 103 | (name, value) => 104 | { 105 | Console.WriteLine($"PowerFxCheck {name}: {value.ToObject()}"); 106 | } 107 | ); 108 | _formulas.Add(result.name); 109 | } 110 | return new(result.name, "Formula", result.check.ReturnType, null); 111 | } 112 | else if (result.isExpression) 113 | { 114 | return new(result.name, "Expression", result.check.ReturnType, null); 115 | } 116 | } 117 | else 118 | { 119 | return new(result.check.ReturnType?.GetType().Name ?? "Error", "", result.check.ReturnType, result.check.Errors.Select(e => new PowerFxSuggestion(e.Kind.ToString(), e.Message, e.Span.Min, e.Span.Lim)).ToArray()); 120 | } 121 | } 122 | return new(result.name, result.text, null, null); 123 | } 124 | catch (Exception ex) 125 | { 126 | return new("Error", "", null, new[] { new PowerFxSuggestion(ex.GetType().Name, ex.Message, 1, 1) }); 127 | } 128 | } 129 | 130 | public (PowerFxResult Result, string Output) Execute(string expr) 131 | { 132 | Debug.WriteLine($"Execute: {expr}"); 133 | var output = new StringBuilder(); 134 | var addLine = (string line) => { output.AppendLine(line); return line; }; 135 | try 136 | { 137 | var check = checkExpression(expr); 138 | if (check.check.IsSuccess) 139 | { 140 | if (check.isAssignment) 141 | { 142 | if (!_variables.Contains(check.name)) 143 | { 144 | _variables.Add(check.name); 145 | } 146 | var value = _engine.Eval(check.text); 147 | _engine.UpdateVariable(check.name, value); 148 | var result = createResult(check.name, value); 149 | addLine($"{check.name} -> {result.FormattedText}"); 150 | return (result, output.ToString()); 151 | } 152 | else if (check.isFormula) 153 | { 154 | if (!_formulas.Contains(check.name)) 155 | { 156 | _engine.SetFormula(check.name, check.text, 157 | (name, value) => addLine(createResult(name, value).FormattedText) 158 | ); 159 | _formulas.Add(check.name); 160 | } 161 | var value = _engine.GetValue(check.name); 162 | var result = createResult(check.name, value); 163 | addLine($"{check.name} = {result.FormattedText}"); 164 | return (result, output.ToString()); 165 | } 166 | else if (check.isExpression) 167 | { 168 | var value = _engine.Eval(check.text); 169 | var result = createResult(check.name, value); 170 | addLine(result.FormattedText); 171 | return (result, output.ToString()); 172 | } 173 | else if (!string.IsNullOrWhiteSpace(expr)) 174 | { 175 | return (createError("Not Recognized"), output.ToString()); 176 | } 177 | else 178 | { 179 | return (createError("Empty"), output.ToString()); 180 | } 181 | } 182 | else 183 | { 184 | var errors = check.check.Errors.Select(e => new PowerFxSuggestion(e.Kind.ToString(), e.Message, e.Span.Min, e.Span.Lim)).ToArray(); 185 | return (createError(string.Join("\n", errors.Select(e => e.ToString()))), check.text); 186 | } 187 | } 188 | catch (Exception ex) 189 | { 190 | return (createError(ex.ToString()), output.ToString()); 191 | } 192 | } 193 | 194 | private (bool isAssignment, bool isFormula, bool isExpression, string name, string text, CheckResult check) checkExpression(string expr) 195 | { 196 | Match match; 197 | // variable assignment: Set( , ) 198 | if ((match = Regex.Match(expr, @"^\s*Set\(\s*(?\w+)\s*,\s*(?.*)\)\s*$")).Success) 199 | { 200 | var name = match.Groups["ident"].Value; 201 | var text = match.Groups["expr"].Value; 202 | return (true, false, false, name, text, _engine.Check(text)); 203 | } 204 | // formula definition: = 205 | else if ((match = Regex.Match(expr, @"^\s*(?\w+)\s*=(?.*)$")).Success) 206 | { 207 | var name = match.Groups["ident"].Value; 208 | var text = match.Groups["formula"].Value; 209 | return (false, true, false, name, text, _engine.Check(text)); 210 | } 211 | // everything except single line comments 212 | else if (!Regex.IsMatch(expr, @"^\s*//") && Regex.IsMatch(expr, @"\w")) 213 | { 214 | return (false, false, true, "", expr, _engine.Check(expr)); 215 | } 216 | else 217 | { 218 | return (false, false, false, "", expr, null); 219 | } 220 | } 221 | 222 | private PowerFxError createError(string name, ErrorValue errorValue) 223 | { 224 | var error = errorValue.Errors[0]; 225 | return new(error.Span?.Min ?? 1, error.Span?.Lim ?? 1, error.Kind, error.Severity, error.Message, name, errorValue); 226 | } 227 | 228 | private PowerFxResult createError(string message) 229 | { 230 | return createResult("Error", FormulaValue.NewError(new ExpressionError 231 | { 232 | Kind = ErrorKind.Unknown, 233 | Severity = DocumentErrorSeverity.Warning, 234 | Message = message 235 | })); 236 | } 237 | 238 | private PowerFxResult createResult(string name, FormulaValue result) 239 | { 240 | return result switch 241 | { 242 | ErrorValue errorValue => createError(name, errorValue), 243 | _ => new PowerFxResult(name, PrintResult(result), result) 244 | }; 245 | } 246 | 247 | private string PrintResult(object value) 248 | { 249 | string resultString = ""; 250 | 251 | if (value is RecordValue record) 252 | { 253 | var separator = ""; 254 | resultString = "{"; 255 | foreach (var field in record.Fields) 256 | { 257 | resultString += separator + $"{field.Name}:"; 258 | resultString += PrintResult(field.Value); 259 | separator = ", "; 260 | } 261 | resultString += "}"; 262 | } 263 | else if (value is TableValue table) 264 | { 265 | int valueSeen = 0, recordsSeen = 0; 266 | string separator = ""; 267 | 268 | // check if the table can be represented in simpler [ ] notation, 269 | // where each element is a record with a field named Value. 270 | foreach (var row in table.Rows) 271 | { 272 | recordsSeen++; 273 | if (row.Value is RecordValue scanRecord) 274 | { 275 | foreach (var field in scanRecord.Fields) 276 | if (field.Name == "Value") 277 | { 278 | valueSeen++; 279 | resultString += separator + PrintResult(field.Value); 280 | separator = ", "; 281 | } 282 | else 283 | valueSeen = 0; 284 | } 285 | else 286 | valueSeen = 0; 287 | } 288 | 289 | if (valueSeen == recordsSeen) 290 | return ("[" + resultString + "]"); 291 | else 292 | { 293 | // no, table is more complex that a single column of Value fields, 294 | // requires full treatment 295 | resultString = "Table("; 296 | separator = ""; 297 | foreach (var row in table.Rows) 298 | { 299 | resultString += separator + PrintResult(row.Value); 300 | separator = ", "; 301 | } 302 | resultString += ")"; 303 | } 304 | } 305 | else if (value is ErrorValue errorValue) 306 | resultString = ""; 307 | else if (value is StringValue str) 308 | resultString = "\"" + str.ToObject().ToString().Replace("\"", "\"\"") + "\""; 309 | else if (value is FormulaValue fv) 310 | resultString = fv.ToObject().ToString(); 311 | else 312 | throw new Exception("unexpected type in PrintResult"); 313 | 314 | return (resultString); 315 | } 316 | } -------------------------------------------------------------------------------- /src/Models/PowerFxGrammar.cs: -------------------------------------------------------------------------------- 1 | namespace PowerNote.Models; 2 | 3 | internal static class PowerFxGrammar 4 | { 5 | public static Dictionary FunctionDescriptions = new() 6 | { 7 | { "Abs", "Absolute value of a number." }, 8 | { "Acceleration", "Reads the acceleration sensor in your device." }, 9 | { "Acos", "Returns the arccosine of a number, in radians." }, 10 | { "Acot", "Returns the arccotangent of a number, in radians." }, 11 | { "AddColumns", "Returns a table with columns added." }, 12 | { "And", "Boolean logic AND. Returns true if all arguments are true. You can also use the && operator." }, 13 | { "App", "Provides information about the currently running app and control over the app's behavior." }, 14 | { "Asin", "Returns the arcsine of a number, in radians." }, 15 | { "Assert", "Evaluates to true or false in a test." }, 16 | { "As", "Names the current record in gallery, form, and record scope functions such as ForAll, With, and Sum." }, 17 | { "AsType", "Treats a record reference as a specific table type." }, 18 | { "Atan", "Returns the arctangent of a number, in radians." }, 19 | { "Atan2", "Returns the arctangent based on an (x,y) coordinate, in radians." }, 20 | { "Average", "Calculates the average of a table expression or a set of arguments." }, 21 | { "Back", "Displays the previous screen." }, 22 | { "Blank", "Returns a blank value that can be used to insert a NULL value in a data source." }, 23 | { "Calendar", "Retrieves information about the calendar for the current locale." }, 24 | { "Char", "Translates a character code into a string." }, 25 | { "Choices", "Returns a table of the possible values for a lookup column." }, 26 | { "Clear", "Deletes all data from a collection." }, 27 | { "ClearCollect", "Deletes all data from a collection and then adds a set of records." }, 28 | { "ClearData", "Clears a collection or all collections from an app host such as a local device." }, 29 | { "Clock", "Retrieves information about the clock for the current locale." }, 30 | { "Coalesce", "Replaces blank values while leaving non" }, 31 | { "Collect", "Creates a collection or adds data to a data source." }, 32 | { "Color", "Sets a property to a built" }, 33 | { "ColorFade", "Fades a color value." }, 34 | { "ColorValue", "Translates a CSS color name or a hex code to a color value." }, 35 | { "Compass", "Returns your compass heading." }, 36 | { "Concat", "Concatenates strings in a data source." }, 37 | { "Concatenate", "Concatenates strings." }, 38 | { "Concurrent", "Evaluates multiple formulas concurrently with one another." }, 39 | { "Connection", "Returns information about your network connection." }, 40 | { "Count", "Counts table records that contain numbers." }, 41 | { "Cos", "Returns the cosine of an angle specified in radians." }, 42 | { "Cot", "Returns the cotangent of an angle specified in radians." }, 43 | { "CountA", "Counts table records that aren't empty." }, 44 | { "CountIf", "Counts table records that satisfy a condition." }, 45 | { "CountRows", "Counts table records." }, 46 | { "DataSourceInfo", "Provides information about a data source." }, 47 | { "Date", "Returns a date/time value, based on Year, Month, and Day values." }, 48 | { "DateAdd", "Adds days, months, quarters, or years to a date/time value." }, 49 | { "DateDiff", "Subtracts two date values, and shows the result in days, months, quarters, or years." }, 50 | { "DateTimeValue", "Converts a date and time string to a date/time value." }, 51 | { "DateValue", "Converts a date" }, 52 | { "Day", "Retrieves the day portion of a date/time value." }, 53 | { "Defaults", "Returns the default values for a data source." }, 54 | { "Degrees", "Converts radians to degrees." }, 55 | { "DisableLocation", "Disables a signal, such as Location for reading the GPS." }, 56 | { "Distinct", "Summarizes records of a table, removing duplicates." }, 57 | { "Download", "Downloads a file from the web to the local device." }, 58 | { "DropColumns", "Returns a table with one or more columns removed." }, 59 | { "EditForm", "Resets a form control for editing of an item." }, 60 | { "EnableLocation", "Enables a signal, such as Location for reading the GPS." }, 61 | { "EncodeUrl", "Encodes special characters using URL encoding." }, 62 | { "EndsWith", "Checks whether a text string ends with another text string." }, 63 | { "Errors", "Provides error information for previous changes to a data source." }, 64 | { "exactin", "Checks if a text string is contained within another text string or table, case dependent. Also used to check if a record is in a table." }, 65 | { "Exit", "Exits the currently running app and optionally signs out the current user." }, 66 | { "Exp", "Returns e raised to a power." }, 67 | { "Filter", "Returns a filtered table based on one or more criteria." }, 68 | { "Find", "Checks whether one string appears within another and returns the location." }, 69 | { "First", "Returns the first record of a table." }, 70 | { "FirstN", "Returns the first set of records (N records) of a table." }, 71 | { "ForAll", "Calculates values and performs actions for all records of a table." }, 72 | { "GroupBy", "Returns a table with records grouped together." }, 73 | { "GUID", "Converts a GUID string to a GUID value or creates a new GUID value." }, 74 | { "HashTags", "Extracts the hashtags (#strings) from a string." }, 75 | { "Hour", "Returns the hour portion of a date/time value." }, 76 | { "If", "Returns one value if a condition is true and another value if not." }, 77 | { "IfError", "Detects errors and provides an alternative value or takes action." }, 78 | { "in", "Checks if a text string is contained within another text string or table, case independent. Also used to check if a record is in a table." }, 79 | { "Int", "Rounds down to the nearest integer." }, 80 | { "IsBlank", "Checks for a blank value." }, 81 | { "IsBlankOrError", "Checks for a blank value or error." }, 82 | { "IsEmpty", "Checks for an empty table." }, 83 | { "IsError", "Checks for an error." }, 84 | { "IsMatch", "Checks a string against a pattern. Regular expressions can be used." }, 85 | { "IsNumeric", "Checks for a numeric value." }, 86 | { "ISOWeekNum", "Returns the ISO week number of a date/time value." }, 87 | { "IsToday", "Checks whether a date/time value is sometime today." }, 88 | { "IsType", "Checks whether a record reference refers to a specific table type." }, 89 | { "JSON", "Generates a JSON text string for a table, a record, or a value." }, 90 | { "Language", "Returns the language tag of the current user." }, 91 | { "Last", "Returns the last record of a table." }, 92 | { "LastN", "Returns the last set of records (N records) of a table." }, 93 | { "Launch", "Launches a webpage or a canvas app." }, 94 | { "Left", "Returns the left" }, 95 | { "Len", "Returns the length of a string." }, 96 | { "Ln", "Returns the natural log." }, 97 | { "LoadData", "Loads a collection from an app host such as a local device." }, 98 | { "Location", "Returns your location as a map coordinate by using the Global Positioning System (GPS) and other information." }, 99 | { "LookUp", "Looks up a single record in a table based on one or more criteria." }, 100 | { "Lower", "Converts letters in a string of text to all lowercase." }, 101 | { "Match", "Extracts a substring based on a pattern. Regular expressions can be used." }, 102 | { "MatchAll", "Extracts multiple substrings based on a pattern. Regular expressions can be used." }, 103 | { "Max", "Maximum value of a table expression or a set of arguments." }, 104 | { "Mid", "Returns the middle portion of a string." }, 105 | { "Min", "Minimum value of a table expression or a set of arguments." }, 106 | { "Minute", "Retrieves the minute portion of a date/time value." }, 107 | { "Mod", "Returns the remainder after a dividend is divided by a divisor." }, 108 | { "Month", "Retrieves the month portion of a date/time value." }, 109 | { "Navigate", "Changes which screen is displayed." }, 110 | { "NewForm", "Resets a form control for creation of an item." }, 111 | { "Not", "Boolean logic NOT. Returns true if its argument is false, and returns false if its argument is true. You can also use the ! operator." }, 112 | { "Notify", "Displays a banner message to the user." }, 113 | { "Now", "Returns the current date/time value." }, 114 | { "Or", "Boolean logic OR. Returns true if any of its arguments are true. You can also use the || operator." }, 115 | { "Param", "Access parameters passed to a canvas app when launched." }, 116 | { "Parent", "Provides access to a container control's properties." }, 117 | { "Patch", "Modifies or creates a record in a data source, or merges records outside of a data source." }, 118 | { "Pi", "Returns the number π." }, 119 | { "PlainText", "Removes HTML and XML tags from a string." }, 120 | { "Power", "Returns a number raised to a power. You can also use the ^ operator." }, 121 | { "Proper", "Converts the first letter of each word in a string to uppercase, and converts the rest to lowercase." }, 122 | { "Radians", "Converts degrees to radians." }, 123 | { "Rand", "Returns a pseudo" }, 124 | { "ReadNFC", "Reads a Near Field Communication (NFC) tag." }, 125 | { "RecordInfo", "Provides information about a record of a data source." }, 126 | { "Refresh", "Refreshes the records of a data source." }, 127 | { "Relate", "Relates records of two tables through a one" }, 128 | { "Remove", "Removes one or more specific records from a data source." }, 129 | { "RemoveIf", "Removes records from a data source based on a condition." }, 130 | { "RenameColumns", "Renames columns of a table." }, 131 | { "Replace", "Replaces part of a string with another string, by starting position of the string." }, 132 | { "RequestHide", "Hides a SharePoint form." }, 133 | { "Reset", "Resets an input control to its default value, discarding any user changes." }, 134 | { "ResetForm", "Resets a form control for editing of an existing item." }, 135 | { "Revert", "Reloads and clears errors for the records of a data source." }, 136 | { "RGBA", "Returns a color value for a set of red, green, blue, and alpha components." }, 137 | { "Right", "Returns the right" }, 138 | { "Round", "Rounds to the closest number." }, 139 | { "RoundDown", "Rounds down to the largest previous number." }, 140 | { "RoundUp", "Rounds up to the smallest next number." }, 141 | { "SaveData", "Saves a collection to an app host such as a local device." }, 142 | { "Search", "Finds records in a table that contain a string in one of their columns." }, 143 | { "Second", "Retrieves the second portion of a date/time value." }, 144 | { "Select", "Simulates a select action on a control, causing the OnSelect formula to be evaluated." }, 145 | { "Self", "Provides access to the properties of the current control." }, 146 | { "Sequence", "Generate a table of sequential numbers, useful when iterating with ForAll." }, 147 | { "Set", "Sets the value of a global variable." }, 148 | { "SetFocus", "Moves input focus to a specific control." }, 149 | { "SetProperty", "Simulates interactions with input controls." }, 150 | { "ShowColumns", "Returns a table with only selected columns." }, 151 | { "Shuffle", "Randomly reorders the records of a table." }, 152 | { "Sin", "Returns the sine of an angle specified in radians." }, 153 | { "Sort", "Returns a sorted table based on a formula." }, 154 | { "SortByColumns", "Returns a sorted table based on one or more columns." }, 155 | { "Split", "Splits a text string into a table of substrings." }, 156 | { "Sqrt", "Returns the square root of a number." }, 157 | { "StartsWith", "Checks if a text string begins with another text string." }, 158 | { "StdevP", "Returns the standard deviation of its arguments." }, 159 | { "Substitute", "Replaces part of a string with another string, by matching strings." }, 160 | { "SubmitForm", "Saves the item in a form control to the data source." }, 161 | { "Sum", "Calculates the sum of a table expression or a set of arguments." }, 162 | { "Switch", "Matches with a set of values and then evaluates a corresponding formula." }, 163 | { "Table", "Creates a temporary table." }, 164 | { "Tan", "Returns the tangent of an angle specified in radians." }, 165 | { "Text", "Converts any value and formats a number or date/time value to a string of text." }, 166 | { "ThisItem", "Returns the record for the current item in a gallery or form control." }, 167 | { "ThisRecord", "Returns the record for the current item in a record scope function, such as ForAll, With, and Sum." }, 168 | { "Time", "Returns a date/time value, based on Hour, Minute, and Second values." }, 169 | { "TimeValue", "Converts a time" }, 170 | { "TimeZoneOffset", "Returns the difference between UTC and the user's local time in minutes." }, 171 | { "Today", "Returns the current date/time value." }, 172 | { "Trace", "Provide additional information in your test results." }, 173 | { "Trim", "Removes extra spaces from the ends and interior of a string of text." }, 174 | { "TrimEnds", "Removes extra spaces from the ends of a string of text only." }, 175 | { "Trunc", "Truncates the number to only the integer portion by removing any decimal portion." }, 176 | { "Ungroup", "Removes a grouping." }, 177 | { "Unrelate", "Unrelates records of two tables from a one" }, 178 | { "Update", "Replaces a record in a data source." }, 179 | { "UpdateContext", "Sets the value of one or more context variables of the current screen." }, 180 | { "UpdateIf", "Modifies a set of records in a data source based on a condition." }, 181 | { "Upper", "Converts letters in a string of text to all uppercase." }, 182 | { "User", "Returns information about the current user." }, 183 | { "Validate", "Checks whether the value of a single column or a complete record is valid for a data source." }, 184 | { "Value", "Converts a string to a number." }, 185 | { "VarP", "Returns the variance of its arguments." }, 186 | { "ViewForm", "Resets a form control for viewing of an existing item." }, 187 | { "Weekday", "Retrieves the weekday portion of a date/time value." }, 188 | { "WeekNum", "Returns the week number of a date/time value." }, 189 | { "With", "Calculates values and performs actions for a single record, including inline records of named values." }, 190 | { "Year", "Retrieves the year portion of a date/time value." }, 191 | }; 192 | 193 | } -------------------------------------------------------------------------------- /src/Modules/MonacoEditor.ts: -------------------------------------------------------------------------------- 1 | declare var require 2 | 3 | declare namespace monaco { 4 | export var KeyMod 5 | export var KeyCode 6 | export var Uri 7 | export var languages 8 | export namespace editor { 9 | export interface ICommandHandler { 10 | (...args: any[]): void; 11 | } 12 | export interface IDisposable { 13 | dispose(): void; 14 | } 15 | export interface IEvent { 16 | (listener: (e: T) => any, thisArg?: any): IDisposable 17 | } 18 | export interface Command { 19 | id: string 20 | title: string 21 | tooltip?: string 22 | arguments?: any[] 23 | } 24 | export interface CodeLens { 25 | range: IRange 26 | id?: string 27 | command?: Command 28 | } 29 | export interface CodeLensList { 30 | lenses: CodeLens[] 31 | dispose(): void 32 | } 33 | export interface CodeLensProvider { 34 | onDidChange?: IEvent 35 | provideCodeLenses(model: editor.ITextModel): Promise | CodeLensList 36 | resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens): Promise | CodeLens 37 | } 38 | export interface IViewZone { 39 | heightInLines: number 40 | heightInPx?: number 41 | afterLineNumber: number 42 | domNode: HTMLElement 43 | marginDomNode: HTMLElement 44 | suppressMouseDown?: boolean 45 | } 46 | export interface IViewZoneChangeAccessor { 47 | layoutZone(zone: string) 48 | addZone(view: IViewZone) 49 | removeZone(arg0: string) 50 | } 51 | export interface ITextModel { 52 | getLinesContent() 53 | } 54 | export interface IModelDeltaDecoration { 55 | id: string 56 | range: IRange 57 | options: { 58 | hoverMessage?: { value: string }, 59 | isWholeLine?: boolean 60 | className?: string 61 | linesDecorationsClassName?: string 62 | inlineClassName?: string 63 | } 64 | } 65 | export interface IStandaloneCodeEditor { 66 | onDidChangeModelContent(arg0: (e: any) => Promise) 67 | getLineDecorations(lineNumber: number): IModelDeltaDecoration[] | null 68 | deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[] 69 | addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null 70 | } 71 | export interface IStandaloneEditorConstructionOptions { } 72 | export interface IModelContentChange { 73 | range: any 74 | } 75 | export function createModel(language: string, text: string, uri: any) 76 | export function create(container: HTMLElement, options: IStandaloneEditorConstructionOptions) 77 | } 78 | export interface IRange { 79 | startLineNumber: number 80 | startColumn: number 81 | endLineNumber: number 82 | endColumn: number 83 | } 84 | export class Range implements IRange { 85 | public startLineNumber: number 86 | public startColumn: number 87 | public endLineNumber: number 88 | public endColumn: number 89 | constructor( 90 | startLine: number, 91 | startColumn: number, 92 | endLine: number, 93 | endColumn: number 94 | ) 95 | } 96 | } 97 | 98 | interface CodeExecutionResult { 99 | code: string 100 | name: string 101 | text: string 102 | type: 'BlankType' | 'BooleanType' | 'NumberType' | 'StringType' | 'TimeType' | 'DateType' | 'DateTimeType' | 'DateTimeNoTimeZoneType' | 'OptionSetValueType' | 'Error' 103 | errors: { 104 | text: string 105 | description: string 106 | start: number 107 | end: number 108 | }[] 109 | } 110 | 111 | interface CodeSuggestionResult { 112 | text: string 113 | description: string 114 | startColumn?: number 115 | endColumn?: number 116 | } 117 | 118 | function createHandler(text: string): (value: A) => T { 119 | return new Function("value", `return ${text}`) as (value: A) => T 120 | } 121 | 122 | var _updateMonacoEditor: (code: string) => void 123 | export function updateMonacoEditor(code: string) { 124 | if (typeof (_updateMonacoEditor) === 'function') { 125 | _updateMonacoEditor(code) 126 | } 127 | } 128 | 129 | export function loadMonacoEditor(id: string, code: string, onChangeHandler: string, onHoverHandler: string, onSuggestHandler: string, onSaveHandler: string, onExecuteHandler: string, onPreviewHandler: string) { 130 | const onChange = createHandler(onChangeHandler) 131 | const onHover = createHandler(onHoverHandler) 132 | const onSuggest = createHandler(onSuggestHandler) 133 | const onSave = createHandler(onSaveHandler) 134 | const onExecute = createHandler(onExecuteHandler) 135 | const onPreview = createHandler(onPreviewHandler) 136 | const container = document.getElementById(id) 137 | const language = "PowerFx" 138 | const languageExt = "pfx" 139 | const theme = "vs-dark" 140 | const loaderScript = document.createElement("script") 141 | loaderScript.src = "https://www.typescriptlang.org/js/vs.loader.js" 142 | loaderScript.async = true 143 | loaderScript.onload = () => { 144 | require.config({ 145 | paths: { 146 | vs: "https://typescript.azureedge.net/cdn/4.0.5/monaco/min/vs", 147 | sandbox: "https://www.typescriptlang.org/js/sandbox" 148 | }, 149 | ignoreDuplicateModules: ["vs/editor/editor.main"] 150 | }) 151 | require(["vs/editor/editor.main", "sandbox/index"], async (editorMain, sandboxFactory) => { 152 | monaco.languages.register({ 153 | id: language, 154 | aliases: [languageExt], 155 | extensions: [languageExt] 156 | }) 157 | 158 | const model = monaco.editor.createModel(code, language, monaco.Uri.parse(`file:///index.pfx`)) 159 | model.setValue(code) 160 | 161 | addMonacoEditorSuggestions(monaco, language, (text, column) => { 162 | const items = [] 163 | const suggested = onSuggest(text) 164 | for (let suggest of suggested) { 165 | items.push({ 166 | startColumn: column, 167 | text: suggest.text, 168 | description: suggest.description 169 | }) 170 | } 171 | return items 172 | }) 173 | 174 | addMonacoEditorHover(monaco, language, (text, column) => onHover(text)) 175 | 176 | const editor = monaco.editor.create(container, { 177 | model, 178 | language, 179 | theme, 180 | ...defaultMonacoEditorOptions(), 181 | lineNumbers: (lineNumber) => { 182 | const lines = model.getLinesContent() 183 | var line = 0; 184 | for (var i = 0; i < lines.length && i + 1 < lineNumber; i++) { 185 | if (lines[i] && lines[i].trim()) { 186 | line += 1; 187 | } 188 | } 189 | const content = model.getLineContent(lineNumber) 190 | if (content && content.trim()) { 191 | return (line + 1).toString() 192 | } else { 193 | return "" 194 | } 195 | } 196 | }) 197 | 198 | _updateMonacoEditor = (code) => editor.setValue(code) 199 | 200 | monaco.languages.registerCodeLensProvider(language, new MonacoCodeLensProvider(editor, onChange, onExecute, onPreview)); 201 | 202 | editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => onSave(editor.getValue())) 203 | 204 | window.addEventListener("resize", () => editor.layout()) 205 | }) 206 | } 207 | document.body.appendChild(loaderScript) 208 | } 209 | 210 | function addMonacoEditorHover(monaco, language, onHover) { 211 | monaco.languages.registerHoverProvider(language, { 212 | provideHover: async (model, position) => { 213 | const line = model.getLineContent(position.lineNumber) 214 | const hover = await onHover(line, position.column) 215 | if (hover) { 216 | return { 217 | contents: [{ value: `## ${hover.text}\n${hover.description}` }], 218 | range: { 219 | startLineNumber: position.lineNumber, 220 | endLineNumber: position.lineNumber, 221 | startColumn: hover.startColumn || 1, 222 | endColumn: hover.endColumn || line.length 223 | } 224 | } 225 | } 226 | } 227 | }) 228 | } 229 | 230 | function addMonacoEditorSuggestions(monaco, language, onSuggest) { 231 | monaco.languages.registerCompletionItemProvider(language, { 232 | provideCompletionItems: async (model, position) => { 233 | const textUntilPosition = model.getValueInRange({ 234 | startLineNumber: position.lineNumber, 235 | startColumn: 1, 236 | endLineNumber: position.lineNumber, 237 | endColumn: position.column 238 | }) 239 | const lineSuggestions = await onSuggest(textUntilPosition, position.column) 240 | if (lineSuggestions && lineSuggestions.length > 0) { 241 | const word = model.getWordUntilPosition(position) 242 | const suggestions = [] 243 | for (const suggestion of lineSuggestions) { 244 | suggestions.push({ 245 | label: suggestion.text, 246 | detail: suggestion.description, 247 | kind: monaco.languages.CompletionItemKind.Value, 248 | insertText: suggestion.text, 249 | range: { 250 | startLineNumber: position.lineNumber, 251 | endLineNumber: position.lineNumber, 252 | startColumn: suggestion.startColumn || word.startColumn, 253 | endColumn: suggestion.endColumn || word.endColumn 254 | } 255 | }) 256 | } 257 | return { suggestions } 258 | } 259 | } 260 | }) 261 | } 262 | 263 | function defaultMonacoEditorOptions(): monaco.editor.IStandaloneEditorConstructionOptions { 264 | return { 265 | lineHeight: 30, 266 | fontSize: 22, 267 | renderLineHighlight: 'all', 268 | wordWrap: 'on', 269 | scrollBeyondLastLine: false, 270 | minimap: { 271 | enabled: false 272 | }, 273 | renderValidationDecorations: 'off', 274 | lineDecorationsWidth: 0, 275 | glyphMargin: false, 276 | contextmenu: false, 277 | codeLens: true, 278 | mouseWheelZoom: true, 279 | quickSuggestions: false, 280 | suggest: { 281 | showIssues: false, 282 | shareSuggestSelections: false, 283 | showIcons: false, 284 | showMethods: false, 285 | showFunctions: false, 286 | showVariables: false, 287 | showKeywords: false, 288 | showWords: false, 289 | showClasses: false, 290 | showColors: false, 291 | showConstants: false, 292 | showConstructors: false, 293 | showEnumMembers: false, 294 | showEnums: false, 295 | showEvents: false, 296 | showFields: false, 297 | showFiles: false, 298 | showFolders: false, 299 | showInterfaces: false, 300 | showModules: false, 301 | showOperators: false, 302 | showProperties: false, 303 | showReferences: false, 304 | showSnippets: false, 305 | showStructs: false, 306 | showTypeParameters: false, 307 | showUnits: false, 308 | showValues: true, 309 | filterGraceful: false 310 | } 311 | } 312 | } 313 | 314 | class MonacoCodeLensProvider implements monaco.editor.CodeLensProvider { 315 | private _decorations: string[] 316 | private _errorCommand: string 317 | private _previewCommand: string 318 | 319 | constructor( 320 | private editor: monaco.editor.IStandaloneCodeEditor, 321 | private check: (text: string) => CodeExecutionResult, 322 | private execute: (text: string) => CodeExecutionResult, 323 | preview: (output: string) => void 324 | ) { 325 | this._errorCommand = editor.addCommand(0, function (_,result: CodeExecutionResult) { 326 | preview("Error: " + result.text) 327 | }, ''); 328 | this._previewCommand = editor.addCommand(1, function (_,result: CodeExecutionResult) { 329 | console.log('_previewCommand',arguments) 330 | preview(result.text?.trim()) 331 | }, ''); 332 | } 333 | onDidChange?: monaco.editor.IEvent 334 | resolveCodeLens?(model: monaco.editor.ITextModel, codeLens: monaco.editor.CodeLens) { 335 | return codeLens 336 | } 337 | async provideCodeLenses(model: monaco.editor.ITextModel) { 338 | const lines = model.getLinesContent() 339 | const decorations: monaco.editor.IModelDeltaDecoration[] = [] 340 | const lenses: monaco.editor.CodeLens[] = [] 341 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 342 | const lineNum = lineIndex + 1 343 | const expr = lines[lineIndex] 344 | if (expr) { 345 | //TODO: Only process lines that have changed... (memoization) 346 | const result = await this.check(expr) 347 | console.log("check", { ...result }) 348 | if (result.errors) { 349 | const errors: string[] = [] 350 | for (var err of result.errors) { 351 | console.log("error", err.description) 352 | 353 | decorations.push({ 354 | id: `error_${lineNum}_${err.start}_${err.end}`, 355 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1), 356 | options: { 357 | inlineClassName: "powerfx-error", 358 | hoverMessage: { value: err.description } 359 | } 360 | }) 361 | errors.push(err.description) 362 | } 363 | result.text = errors.join('\n') 364 | lenses.push({ 365 | range: new monaco.Range(lineNum, err.start + 1, lineNum, err.end + 1), 366 | id: `preview_${lineNum}_${err.start}_${err.end}`, 367 | command: { 368 | id: this._errorCommand, 369 | title: "Error(s)", 370 | tooltip: result.text, 371 | arguments: [result] 372 | } 373 | }) 374 | } else if (result.type !== "BlankType") { 375 | const output = await this.execute(expr) 376 | console.log("execute", { ...output }) 377 | decorations.push({ 378 | id: `type_${lineNum}`, 379 | range: new monaco.Range(lineNum, 1, lineNum, 1), 380 | options: { 381 | inlineClassName: "powerfx-type", 382 | hoverMessage: { value: result.type } 383 | } 384 | }) 385 | lenses.push({ 386 | range: new monaco.Range(lineNum, 1, lineNum, 1), 387 | id: `preview_${lineNum}`, 388 | command: { 389 | id: this._previewCommand, 390 | title: "View Result", 391 | tooltip: result.text, 392 | arguments: [output] 393 | } 394 | }) 395 | } 396 | } 397 | } 398 | this._decorations = this.editor.deltaDecorations(this._decorations || [], decorations); 399 | return { 400 | lenses, 401 | dispose: () => { } 402 | } 403 | } 404 | } --------------------------------------------------------------------------------