diff --git a/.babelrc b/.babelrc index f4175971b..753e120b5 100644 --- a/.babelrc +++ b/.babelrc @@ -1,20 +1,20 @@ -{ - "plugins": [ - [ - "module-resolver", - { - "cwd": "babelrc", - "alias": { - "Components": "./app/src/components", - "Containers": "./app/src/containers" - } - } - ] - ], - // presets are a set of of plug-ins - "presets": [ - ["@babel/preset-env", { "targets": { "node": "current" } }], - "@babel/preset-typescript", - "@babel/preset-react" - ] -} +{ + "plugins": [ + [ + "module-resolver", + { + "cwd": "babelrc", + "alias": { + "Components": "./app/src/components", + "Containers": "./app/src/containers" + } + } + ] + ], + // presets are a set of of plug-ins + "presets": [ + ["@babel/preset-env", { "targets": { "node": "current" } }], + "@babel/preset-typescript", + "@babel/preset-react" + ] +} diff --git a/.dockerignore b/.dockerignore index 8361b6aa5..b6f6fb15d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,2 @@ -node_modules +node_modules app/electron \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 9b85387af..c6f33686c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,22 +1,22 @@ -{ - "extends": ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "airbnb-base"], - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2018, - "sourceType": "module" - }, - "plugins": ["import", "react", "jest", "jsx-a11y", "babel"], - "parser": "@babel/eslint-parser", - "env": { - "browser": true, - "node": true, - "es6": true, - "jest": true - }, - "rules": { - "class-methods-use-this": "off", - "linebreak-style": 0 - } -} +{ + "extends": ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "airbnb-base"], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": ["import", "react", "jest", "jsx-a11y", "babel"], + "parser": "@babel/eslint-parser", + "env": { + "browser": true, + "node": true, + "es6": true, + "jest": true + }, + "rules": { + "class-methods-use-this": "off", + "linebreak-style": 0 + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c8972cd16..53b17ef25 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,38 +1,38 @@ -# Description - -Please describe the issue of the pull request and the changes - - - -## Type of Change - -Please check the options that apply - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update - -# How Has the Changes Been Tested? - - - -# Checklist: - -- [ ] My code follows the style guidelines of this project -- [ ] Changes included in this pull request covers minimal topic -- [ ] I have performed a self-review of my code -- [ ] I have commented my code properly, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] New and existing unit tests pass locally with my changes -- [ ] Any dependent changes have been merged and published in downstream modules +# Description + +Please describe the issue of the pull request and the changes + + + +## Type of Change + +Please check the options that apply + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has the Changes Been Tested? + + + +# Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] Changes included in this pull request covers minimal topic +- [ ] I have performed a self-review of my code +- [ ] I have commented my code properly, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ebb780741..06a720c72 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,40 +1,40 @@ -# This is a basic workflow to help you get started with Actions - -name: CI - -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the master branch - push: - branches: [dev] - pull_request: - branches: [dev] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - with: - node-version: 18.13.0 - - # Runs a single command using the runners shell - - name: Install dependencies - run: npm install - - name: Run all tests - run: npm run test --bail - - # Runs a set of commands using the runners shell - - name: Run a multi-line script - run: | - echo Add other actions to build, - echo test, and deploy your project. +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [dev] + pull_request: + branches: [dev] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + with: + node-version: 18.13.0 + + # Runs a single command using the runners shell + - name: Install dependencies + run: npm install + - name: Run all tests + run: npm run test --bail + + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. diff --git a/.gitignore b/.gitignore index 45c80f88a..d0e8929bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,531 +1,531 @@ -# Created by https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn -yarn.lock -.prettierrc.json -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ -dist/ -build/ -release-builds/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# next.js build output -.next - -# nuxt.js build output -.nuxt - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -#!! ERROR: yarn is undefined. Use list command to see defined gitignore types !!# - -### VisualStudio ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -#VSCode -.vscode/ -reactype.code-workspace - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# DMG File -reactype.dmg -installers/ - -# for server key and ssl certificate generation -server/domains.ext -server/localhost.crt -server/localhost.csr -server/localhost.key -server/RootCA.crt -server/rootCA.key -server/rootCA.pem -server/RootCA.srl - - - -# End of https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn - -/test-results/ -/playwright-report/ -/playwright/.cache/ - -#amplify-do-not-edit-begin -amplify/\#current-cloud-backend -amplify/.config/local-* -amplify/logs -amplify/mock-data -amplify/mock-api-resources -amplify/backend/amplify-meta.json -amplify/backend/.temp -build/ -dist/ -node_modules/ -aws-exports.js -awsconfiguration.json -amplifyconfiguration.json -amplifyconfiguration.dart -amplify-build-config.json -amplify-gradle-config.json -amplifytools.xcconfig -.secret-* -**.sample -#amplify-do-not-edit-end - -/amplify/team-provider-info.json - -#TypeScript coverage report +# Created by https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn +yarn.lock +.prettierrc.json +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ +dist/ +build/ +release-builds/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +#!! ERROR: yarn is undefined. Use list command to see defined gitignore types !!# + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +#VSCode +.vscode/ +reactype.code-workspace + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# DMG File +reactype.dmg +installers/ + +# for server key and ssl certificate generation +server/domains.ext +server/localhost.crt +server/localhost.csr +server/localhost.key +server/RootCA.crt +server/rootCA.key +server/rootCA.pem +server/RootCA.srl + + + +# End of https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +#amplify-do-not-edit-begin +amplify/\#current-cloud-backend +amplify/.config/local-* +amplify/logs +amplify/mock-data +amplify/mock-api-resources +amplify/backend/amplify-meta.json +amplify/backend/.temp +build/ +dist/ +node_modules/ +aws-exports.js +awsconfiguration.json +amplifyconfiguration.json +amplifyconfiguration.dart +amplify-build-config.json +amplify-gradle-config.json +amplifytools.xcconfig +.secret-* +**.sample +#amplify-do-not-edit-end + +/amplify/team-provider-info.json + +#TypeScript coverage report coverage-ts/ \ No newline at end of file diff --git a/.npmignore b/.npmignore index b42c82ad7..962999558 100644 --- a/.npmignore +++ b/.npmignore @@ -1,485 +1,485 @@ -# Created by https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride -build/ - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ -dist/ -release-builds/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# next.js build output -.next - -# nuxt.js build output -.nuxt - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -#!! ERROR: yarn is undefined. Use list command to see defined gitignore types !!# - -### VisualStudio ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# DMG File -reactype.dmg -installers/ -assets/ -.git/ - +# Created by https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride +build/ + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ +dist/ +release-builds/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +#!! ERROR: yarn is undefined. Use list command to see defined gitignore types !!# + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# DMG File +reactype.dmg +installers/ +assets/ +.git/ + # End of https://www.gitignore.io/api/node,linux,macos,windows,visualstudio,yarn \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 276a47a73..d06320fb8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ -{ - "printWidth": 80, - "singleQuote": true, - "trailingComma": "none" -} +{ + "printWidth": 80, + "singleQuote": true, + "trailingComma": "none" +} diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index 1fb36e28c..2af2092c0 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -1,389 +1,389 @@ -

- -

ReacType Change Log

-

- -## Version 20.0.0 Changes - -### Changes: - -- **Developer Improvement:** - - Migrated from Webpack to Vite, improving HMR times drastically - - Deployed app using Heroku instead of AWS decreasing time to deployment -- **User Features:** - - **Collaboration Room:** - - Implemented live video, audio, and text functionality using socket.IO - - Added authentication and error handling to joining existing rooms - - **UI updates to enhance user experience:** - - In addition to drag to add, users are now able to click to add - - Updated left panel to include user information and settings - - Added scroll and zoom buttons to canvas. Scroll now automatically scrolls to bottom once enough elements are added - - Updated UI design to reflect a more modern look -- **Bugs Fixed:** - - Canvas - All appropriate elements can now be nested - Nested Elements in the code preview now accurately reflect nested elements. They can also be dragged. - - Bottom Panel - Now opens by click instead of hover - - Users can now delete elements without first clicking it and then the X. This applies to the nested components as well. -- **Landing Page:** - - Revamped entire landing page for a more modern look - -### Recommendations for Future Enhancements: - -- Bug fix for market place preview display -- Fix bottom panel to only close upon clicking the icon, and not anywhere else -- Populate settings tab in the left panel with additional functionality -- Allow users to modify code dynamically in the code preview and reflect visual componenets in real time -- Add zoom in and zoom out / scroll functionality to code preview and component tree -- Convert from 95% to 100% typescript -- Add more functionality to the nav bar -- List all active rooms to join -- Clean up unnecessary code / comments and deprecated libraries -- a tags which are nested do not display accurate code in code preview -- Eliminate all Webpack associated files/folders/dependencies/etc... now that we run on Vite -- Remove the many deprecated dependencies -- Add additional features to the live chat (Links, reactions, raise hand feature etc) -- Allow live chat to be a popup and draggable outside of the app -- Implement MUI/ShadcnUI in addition to standard html elements on left panel so that users are able to start off with pre styled elements -- Make the app mobile responsive. Right now it does not work/look good on mobile -- We had to deploy via Heroku due to time limitations and Vite. We would recommend going back to AWS with dockerized containers. -- Light/Dark mode in the left settings tab -- Update links in the footer of the landing page - -## Version 19.0.0 Changes - -### Changes: - -- **Developer Improvement:** - - Typescript conversion continued and now sits at ~95% -- **User Features:** - - **Collaboration Room:** - - **Bug Fixes:** - - Debug “Leave Room” functionality removing username from the users list - - Debug “Join Room” functionality so the current canvas does not reset upon new user joining collaboration - - Debug Code Preview button that sent error if toggled more than once and does not force toggled view to other users in the room - - Collaboration room is now ready for release (v1) - - Implemented live cursor tracking with on/off function where multiple users can choose to see the other users mouse cursors in real-time in the same canvas. - - Added keydown functionality to “Join Room” by triggering button click on keyboard “Enter” - - Reconfigured web socket initiation to force new connection when joining room - - Refactored the way changes were being passed to other users in the collaboration room - - Significantly reduces the amount of data being passed among users by passing only the payload for each individual action, triggering singular updates for other users in the collaboration environment - - Added Event Emitters for each action that updates canvas - - Created a websocket service layer to maintain a single socket instance throughout the app - - **User List:** - - Displays the username and mouse cursor of all connected users in a particular room with a specific color scheme - - UI updated to enhance user experience: - - Rendered MUI Icons in HTML Element Panel - - Redesigned drag-n-drop to be more intuitive and professionalize application design. - - Updated styling to overall style and theme to maintain consistency across the application - - Removed Tailwind and CSS save buttons in Customization panel for cleaner UI and drying up repetitive functionality - -### Recommendations for Future Enhancements: - -- Fix Undo & Redo functionality. Undo & Redo buttons on the customization page not functioning as expected. -- Update Electron for desktop application use. Resolve electron app functionality to coincide with web app functionality. -- Add Change Log/View Edit History feature in app -- v.17 recommendations regarding the Marketplace are still undeveloped. -- Expand testing coverage. Continue fixing old tests which rely on outdated dependencies, and implementing new tests. -- Continue Typescript conversion. Consider toggling noImplicitAny to find all 'any' cases that can be addressed. -- Continue cleanup of outdated and unused code and files. -- Future teams could look into data structures for scaling on the server side of the app to improve data transmitting and multiple server functionality. -- Continue modularizing code. Many large, unwieldy files that should be broken up into more modular components still exist. -- Fix the reset of context manager and state manager when a user leaves the room. -- Collaboration room: - - Allow for given HTML components to be nested into custom created components - - Collaboration Room feature can be further scaled with AWS servers and clients for better experiences. The feature currently is limited to access with only 1 AWS cloud server. - - True real-time rendering so users can see components as they're being dragged onto the canvas, rather than only when they're placed. - - List of active rooms so users can simply pick one to join. Will likely be paired with a password feature for security, so only users with the proper credentials can join a particular room. - - Chat Feature in Collaboration Room - - Currently, the live tracking cursor is rendered based on the users username/nickname. If multiple users create the same username/nickname, the most recent username/nickname creator will override the former. Possible solution to this issue could be to store cursor with the socket id rather than username/nickname. " - -**Version 18.0.0 Changes** - -Changes:
- -- Developer Improvements: - - Typescript conversion continued and now sits at ~90% - - Dev Bug Fixes: - - Deleted ts-coverage files and added folder to git.ignore so TS conversion status is properly reflected on the GitHub repository. - - Cleaned up outdated code and removed multiple unused and duplicate files, particularly those related to the now-obsolete Dark Mode functionality and some other lingering code from the v.17 migration. - - Modularity: - - Migrated large portions of RoomContainer functionality into smaller components to improve the reusability of code. - - Created more interface types for reusability to multiple parts of the applications. -- User Features: - - Collaboration Room: - - Implemented room functionality where multiple users can see and interact with the same canvas state in real time. - - Dynamically handles the host logic of the collab room, where the oldest connected client is the one serving the room's state. - - Fixed backend web socket connections with the clients, allowing full duplex connections between multiple clients and servers. - - Note: The collaboration room is in beta - - User List: - - List that displays all connected users in a particular room. - - Dynamically updates when users join or leave a room. - - Automatically updates the new host in the room to the next oldest user. - - Join/Nickname Button: - - Allows users to specify which room to join, and what name to display upon joining the room. - - The button only shows when the user is not connected to the room, requiring both fields to be filled out. - -Recommendations for Future Enhancements:
- -- Chat functionality so users in the same room can discuss their projects. -- List of active rooms so users can simply pick one to join. Will likely be paired with a password feature for security, so only users with the proper credentials can join a particular room. -- True real-time rendering so users can see components as they're being dragged onto the canvas, rather than only when they're placed. -- Optimize performance of room state updates -- v.17 recommendations regarding the Marketplace are still undeveloped. -- Solve residual bugs. Undo & Redo buttons on the customization page not functioning as expected. Backend bugs persist as seen in the console when running the dev environment. Persistent Redux error that causes page to rerender more often than necessary. -- Resolve electron app functionality to coincide with web app functionality. -- For the state manager option in the data table there is a MuiData-menu that is not visible when clicking it and after the filter option is clicked it creates a white space in the bottom of the page. -- Expand testing coverage. Continue fixing old tests which rely on outdated dependencies, and implementing new tests. -- Continue modularizing code. Many large, unwieldy files that should be broken up into more modular components still exist. -- Continue Typescript conversion. Consider toggling noImplicitAny to find all 'any' cases that can be addressed. -- Continue cleanup of outdated and unused code and files. -- Collaboration feature still needs to be improved for scalability with AWS servers and clients for better experiences. The feature currently is limited to access with only 1 AWS cloud server. -- Future teams could look into data structures for scaling on the server side of the app to improve data transmitting and multiple server functionality. - -**Version 17.0.0 Changes** - -Changes:
- -- Developer Improvements: - - Testing Coverage: - - Version 17 added testing for the added marketplace-related components - - Testing coverage sits at ~60% - - Typescript continued and now sits at ~80% - - Dev Bug Fixes: - - Additional logic added for edge cases in inputs for state manager (passing in non-Arrays/non-Objects as Array type and Object type). - - Fixed issue with the bottom panel not dragging or sticking to the mouse when the mouse is over the demo render iframe - - Cleaned up hundreds of lines of outdated code and archived multiple unused and duplicate files - - OAuth now linked to standalone Gmail and GitHub accounts -- User Features: - - UI updated with a modern style for a better developer experience - - Added many user feedback alerts for a better experience including alerts for when projects are published, cloned, deleted, HTML custom tags are created, context created, or custom components created. - - Built a specific buttons menu that individually displays the HTML elements, reusable components created, and join room option. - - Redesigned the state manager panel option to be readable and functional. - - Drop down menu now closes only when the user clicks outside of the menu - - Marketplace: - - Implemented a dedicated area for developers to share their projects - - Routing handled by React Router - - Projects can also be cloned to the user's account to be used and edited with the addition of a button - - Added search functionality to search by username and project name - - Included a separate section in the Saved Projects and Delete Projects modal in the Manage Project menu for cloned projects from the Marketplace - - Publish/Unpublish Button: - - Publish feature on the web app allows users to publish their saved project files into the Marketplace from the main app page - - Dynamically switches between publish/unpublish depending on whether the loaded project is in the Marketplace - -Recommendations for Future Enhancements:
- -- Add a comment section and description section for each published project -- Consider maybe a way for users to pull individual components from one project into another -- Use localforage or other methods to store unsaved projects either on logout or accidental closure of the browser, so that when the user opens the browser again it is still there. -- Continue expanding testing coverage. Improve testing by adding additional unit tests, expanding end-to-end testing, and introducing integration testing. -- Continue quality Typescript conversion. Continue to fix type errors within component files. -- Modularize appStateSlice file. Further modularization is needed for readability and maintainability. -- Solve residual bugs. Undo & Redo buttons on customization page not functioning as expected. Backend bugs persist as seen in the console when running the dev environment. Resolve electron app functionality to coincide with web app functionality. -- Take a look at the join room functionality using web sockets in order to allow users to collaborate on the same project at the same time. -- For the state manager option in the data table there is a MuiData-menu that is not visible when clicking it and after the filter option is clicked it creates a white space in the bottom of the page. -- Continue code cleanup. Continue cleanup of outdated and unused code and files - -**Version 16.0.0 Changes** - -Changes:
- -- Developer Improvements: - - Testing Coverage: - - Version 16 introduces end-to-end testing with Playwright and adds additional unit testing with React Testing Library. - - Testing coverage has now doubled since version 15, and now sits at just over 50% coverage. - - Transitioned away from Enzyme to maintain consistency with RTL and Jest. - - Typescript Conversion: - - Upped typescript coverage from 30% to 80%. - - Fixed multiple type errors in component files. - - Added CI pipeline for testing: - - Transitioned away from Travis CI to Github Actions for improved CI pipeline. Github Actions will now run all tests upon each pull request to dev. - - Updated OAuth and Sign In Features: - - Sign in feature now connected to the latest database version. - - Fixed bug that allowed only one OAuth user to sign in at a time. - - Github OAuth is now connected to Adam Vanek. - - Dev Bug Fixes: - - Debugged ‘worker error’ on code preview & style editor by refactoring Ace-Build components. - - Additional logic added for edge cases in inputs for context manager, state manager, and signup features. - - Cleaned up hundreds of lines of outdated code and deleted multiple unused and duplicate files - - Dependency Updates: - - All previously outdated dependencies are now updated. Time it takes for the app to bundle in dev is now cut in half. -- User Features: - - Export Button: - - Export feature on the web app now allows users to download the current project as a zip file with modularized component folder, html, and css file included. - - Export feature is now available to all users including guests. - - CSS Live Rendering: - - CSS Editor changes now rendered visually in the demo page on save. - - UI Changes: - - Fixed multiple contrast issues with white text displaying on white background in State Manger Display tab tables, state management tables, table menu dropdowns, Context Manager tables, and Context Manager display. - - Adjusted context manager interface for improved UX when creating context and saving key/value pairs. - - Fixed border styling within modals and error messages that were cutting off inputs on focus. - - Added save button to customization tab. - - Bug Fixes: - - Manage project features for registered users now successfully saves, opens, and deletes projects. - - State Manager now successfully deletes state from parent components. - - Context Manager display chart renders correctly. - - CSS Editor contents now persist after rerender. - -Recommendations for Future Enhancements:
- -- Refactor away from MUI. MUI is very opinionated and while creating components with it is easy it leaves a lot to be desired. Dark Mode also needs to be improved as color contrast is very low. -- Continue expanding testing coverage. Improve testing by adding additional unit tests, expanding end-to-end testing, and introducing integration testing. -- Continue quality Typescript conversion. Continue to fix type errors within component files. -- Modularize appStateSlice file. Further modularization is needed for readability and maintainability. -- Solve residual bugs. Undo & Redo buttons on customization page not functioning as expected. Backend bugs persist as seen in the console when running the dev environment. Resolve electron app functionality to coincide with web app functionality. -- Continue code cleanup. Continue cleanup of outdated and unused code and files - -**Version 15.0.0 Changes** - -Changes:
- -- Developer Improvements: - - Redux Toolkit: - - Migrated state from a combination of useReducer/useContext and Redux to only using Redux Toolkit. This is the recommended modern approach to handling large state management in this sort of application. Enhances the developer experience by enabling the use of the Redux Devtools to debug, and see state/actions in real-time. - - Dependency Updates - - New developers can easily npm install without having to use an older version of node or using --legacy-peer-deps - - Updated to modern versions to take advantage of newer features -- User Features: - - Websockets: - - Users can now join rooms to collaborate in realtime - - Tailwind CSS: - - In the customization panel users can now choose between inline CSS and Tailwind. These changes are reflected in the live code preview. - - OAuth: - - Users may now log in using OAuth which enhances security, and makes sign in a breeze. - - Deployed Website: - - Containerized and deployed a working version of the application. Instead of having to download an application users may now interact live. - -Recommendations for Future Enhancements:
- -- Continue working on State Management. There are some changes that can be made to make the application cleaner. Right now the appStateSlice is a large file which houses a lot of the reducer functions. We believe there is a way to further modularize this to make it simpler to read, and iterate upon in the future. -- Convert to using Vite. While developing we ran into issues with webpack taking a long time to reflect changes. Vite is lightweight and enhances the developer experience. -- Expand Testing Coverage. Making a large move of state management made a lot of the testing innefective since it was based upon old ways. -- Refactor away from MUI. MUI is very opinionated and while creating components with it is easy it leaves a lot to be desired. -- Residual Bugs. While migrating state there are a few lingering bugs within the application. This process should be easier now with Redux Devtools availability, but we did not have time to go through every action and conduct thorough testing. - -**Version 14.0.0 Changes** - -Changes:
- -- Added functionality & improvements: - - Event listeners: - - Added ability to assign event listeners to elements in the bottom customization tab - - Can name the function on the event or use the default name provided - - Updates reflected in the code preview render - - Live code preview: Bottom tool tabs code preview box updates immediately and automatically to reflect the latest changes in state - - Converted the annotation button into a delete button on the canvas elements and reusable components - - Code preview render: The formatting for generated code has been corrected for improved readability -- Major UI changes: - - Left panel: - - Only display when mouse hovered over - - When extended, floats in front of the canvas without affecting the main window formatting - - Bottom panel - - Retractable feature added - - Has internal scroll ability in the tabs - - Resized functionality is stable - - Added indicator tabs to each signifying to the user their presence - - Canvas container (upper left): - - Changed the formatting to a center column with readable size and label orientation - - Standardized the size of components and rate of growth when nesting - - Tutorial: - - Users can now reference tutorials in split-screen mode without the canvas being auto-cleared when going back and forth from the tutorial -- Bug fixes: - - Reusable component: The drag-and-drop feature for reusable components is now functioning smoothly and without bugs - -Recommendations for Future Enhancements:
- -- Add function content in the current event listeners' function skeleton. -- The code output formatting in generateCode.ts is currently difficult to read, and could be improved for better readability. -- Currently, the project uses two sets of state management tools: useReducer/useContext and Redux. useReducer/useContext is used for handling the customization state, and Redux for managing the code preview, context manager, and dark mode reducer state. However, there seems to be some confusion around how to integrate these two tools effectively. For instance, both tools are used for managing the code preview state, and changing the useReducer/useContext state would replace the corresponding redux state. Need to clean up the logic and find a solution to solve this issue. -- Some of the files structure is not accurate (e.g., customizationPanel.tsx is in the containers folder instead of the bottom folder), need to rearrange the file hierarchy. -- Update packages and resolve package dependency issues. - -**Version 13.0.0 Changes** - -New Functionality:
- -- Manage state locally: Users can now manage state dynamically within nested components using React Hooks within the state manager tab. -- Add/delete props: For a selected component, users can see a list of available props from the parent, add props, and delete props in case they are not - required later on. -- State/props flow: If state or props are deleted upstream, it will automatically update the state for its children components. -- Visualize state/props flow: Within the display sub-tab of the state manager tab, users can visualize an interactive tree diagram depicting the state initialized in the current component and passed down props from the parent component. - -Enhancements:
- -- Live code preview: Live rendering of code based on any changes in the state and dragging and nesting of components. -- Next.js & Gatsby compatibility: New state manager tab is now compatible with next.js and Gatsby. -- Tutorial: Tutorial is functional and has the latest guides to navigate through the newly added state management tab. - -Deployment Updates:
- -- Electron app is now available for Windows users. -- Web based version of the app is available on Heroku. - -Bug Fixes:
- -- User dashboard: The dashboard works now and shows private and shared projects with the ability for users to drop comments. -- Login/logout: Users can now signup/login/logout now on both development and production environments. -- Manage Projects: Github authenticated users are now able to create and save projects. -- Customization: Use State works as expected now within HTML elements. - -What’s next:
- -- Adding on click functionality within components. Goal: Make a fully functional tic-tac-toe app. -- Incorporating material ui into the components so that exported app has visually appealing components. -- Enabling auto save functionality when dragging and dropping components, and amending component state. -- Allowing users to click and access projects within the dashboard for review. -- Adding more integration and E2E testing. -- Fixing bugs in the heroku (web based) deployment: login/logout, GitHub oauth etc. -- Enabling google oauth in all environments. -- Packaging electron app for Linux users. -- Conversion from monolithic to micro services for better scaling in the future. - -**Version 12.0.0 Changes** - -- Context Visualizer: You can now visually see what component is consuming which context. As you click on the interactive tree, the component assigned to the context will be revealed. -- React 18: Updated to React 18 -- Export Feature: Created an exportable context file, integrated with original codebase. Ready to go code: Added boilerplate codes to components based on which contexts they are consuming. - -**A note to future contributors** - -Attempted to implement Facebook and Google OAuth via passport but as of Electron’s current version, neither of them not compatible with electron. - -**Version 11.0.0 Changes:** - -- Added Next.js functionality - - Next.js projects will generate the right code needed for exporting a Next.js application - - Link & Image elements have been added - - Link components are able to couple with a page to enable SSR - - Next Link components have a drop down menu to quickly and easily link pages - - Current canvas can be saved as a page to be coupled with a Next.js Link element - - Files are exported with the appropriate Next.js imports and structure -- Added Redux and began migrating some state over for ease of development (debugging & readability) -- Fixed bug causing electron to crash when closing the window rather than going to file > exit -- Fixed bug causing app to crash when project was changed to either Next.js or Gatsby.js -- Fixed GitHub OAuth - - added Passport.js & Passport-Github libraries for strategies which takes care of all the credential exchanges and session information - - linked electron front end to talk to backend to exchange credentials -- Fixed code preview not displaying properly -- Fixed demo render preview so that changes in the canvas appears instantly - - any links in the demo render preview can now be clicked on and it will take you to its related page -- Properties of each component now persist in the customization tab -- Fixed dark mode not syncing properly across pages - -**Version 11.0.0 Stretch Features:** - -- Move more state away from the react hook & context API and into the Redux store - - This will be very time consuming but will make implementing new features much easier - - Highly suggest you read Redux documentation on best practices before diving into this - - This will improve performance by reducing the amount of unneccessary re-render. The context API causes certain pieces of state to be needlessly coupled - - Debugging is much easier by the use of Redux dev tools which allow time travel debugging - - Code will be easier to read and thus data flow will be easier to visualize - - Don't move **everything** onto Redux. ie: Material UI uses the context API to handle theme changes -- Enable remote work similar to vscode's live share - - Tried to implement peer to peer communication via webRTC with redux swarmlog but was not successful - - Look into using websockets - - Think about security. What features needs to be implemented for secure sharing? - - Transfer actions through websockets via Redux middleware (Thunk)? -- Save project (state) onto local storage for guests -- Redesign UI to be more flexible - - Read material ui docs for best practices. - - creation panel should be redesigned. Its react component structure is too fragmented. -- Add missing Next.js features - - Image components need sizing & loading options to capitalize on Next.js' Image optimization +

+ +

ReacType Change Log

+

+ +## Version 20.0.0 Changes + +### Changes: + +- **Developer Improvement:** + - Migrated from Webpack to Vite, improving HMR times drastically + - Deployed app using Heroku instead of AWS decreasing time to deployment +- **User Features:** + - **Collaboration Room:** + - Implemented live video, audio, and text functionality using socket.IO + - Added authentication and error handling to joining existing rooms + - **UI updates to enhance user experience:** + - In addition to drag to add, users are now able to click to add + - Updated left panel to include user information and settings + - Added scroll and zoom buttons to canvas. Scroll now automatically scrolls to bottom once enough elements are added + - Updated UI design to reflect a more modern look +- **Bugs Fixed:** + - Canvas - All appropriate elements can now be nested - Nested Elements in the code preview now accurately reflect nested elements. They can also be dragged. + - Bottom Panel - Now opens by click instead of hover + - Users can now delete elements without first clicking it and then the X. This applies to the nested components as well. +- **Landing Page:** + - Revamped entire landing page for a more modern look + +### Recommendations for Future Enhancements: + +- Bug fix for market place preview display +- Fix bottom panel to only close upon clicking the icon, and not anywhere else +- Populate settings tab in the left panel with additional functionality +- Allow users to modify code dynamically in the code preview and reflect visual componenets in real time +- Add zoom in and zoom out / scroll functionality to code preview and component tree +- Convert from 95% to 100% typescript +- Add more functionality to the nav bar +- List all active rooms to join +- Clean up unnecessary code / comments and deprecated libraries +- a tags which are nested do not display accurate code in code preview +- Eliminate all Webpack associated files/folders/dependencies/etc... now that we run on Vite +- Remove the many deprecated dependencies +- Add additional features to the live chat (Links, reactions, raise hand feature etc) +- Allow live chat to be a popup and draggable outside of the app +- Implement MUI/ShadcnUI in addition to standard html elements on left panel so that users are able to start off with pre styled elements +- Make the app mobile responsive. Right now it does not work/look good on mobile +- We had to deploy via Heroku due to time limitations and Vite. We would recommend going back to AWS with dockerized containers. +- Light/Dark mode in the left settings tab +- Update links in the footer of the landing page + +## Version 19.0.0 Changes + +### Changes: + +- **Developer Improvement:** + - Typescript conversion continued and now sits at ~95% +- **User Features:** + - **Collaboration Room:** + - **Bug Fixes:** + - Debug “Leave Room” functionality removing username from the users list + - Debug “Join Room” functionality so the current canvas does not reset upon new user joining collaboration + - Debug Code Preview button that sent error if toggled more than once and does not force toggled view to other users in the room + - Collaboration room is now ready for release (v1) + - Implemented live cursor tracking with on/off function where multiple users can choose to see the other users mouse cursors in real-time in the same canvas. + - Added keydown functionality to “Join Room” by triggering button click on keyboard “Enter” + - Reconfigured web socket initiation to force new connection when joining room + - Refactored the way changes were being passed to other users in the collaboration room + - Significantly reduces the amount of data being passed among users by passing only the payload for each individual action, triggering singular updates for other users in the collaboration environment + - Added Event Emitters for each action that updates canvas + - Created a websocket service layer to maintain a single socket instance throughout the app + - **User List:** + - Displays the username and mouse cursor of all connected users in a particular room with a specific color scheme + - UI updated to enhance user experience: + - Rendered MUI Icons in HTML Element Panel + - Redesigned drag-n-drop to be more intuitive and professionalize application design. + - Updated styling to overall style and theme to maintain consistency across the application + - Removed Tailwind and CSS save buttons in Customization panel for cleaner UI and drying up repetitive functionality + +### Recommendations for Future Enhancements: + +- Fix Undo & Redo functionality. Undo & Redo buttons on the customization page not functioning as expected. +- Update Electron for desktop application use. Resolve electron app functionality to coincide with web app functionality. +- Add Change Log/View Edit History feature in app +- v.17 recommendations regarding the Marketplace are still undeveloped. +- Expand testing coverage. Continue fixing old tests which rely on outdated dependencies, and implementing new tests. +- Continue Typescript conversion. Consider toggling noImplicitAny to find all 'any' cases that can be addressed. +- Continue cleanup of outdated and unused code and files. +- Future teams could look into data structures for scaling on the server side of the app to improve data transmitting and multiple server functionality. +- Continue modularizing code. Many large, unwieldy files that should be broken up into more modular components still exist. +- Fix the reset of context manager and state manager when a user leaves the room. +- Collaboration room: + - Allow for given HTML components to be nested into custom created components + - Collaboration Room feature can be further scaled with AWS servers and clients for better experiences. The feature currently is limited to access with only 1 AWS cloud server. + - True real-time rendering so users can see components as they're being dragged onto the canvas, rather than only when they're placed. + - List of active rooms so users can simply pick one to join. Will likely be paired with a password feature for security, so only users with the proper credentials can join a particular room. + - Chat Feature in Collaboration Room + - Currently, the live tracking cursor is rendered based on the users username/nickname. If multiple users create the same username/nickname, the most recent username/nickname creator will override the former. Possible solution to this issue could be to store cursor with the socket id rather than username/nickname. " + +**Version 18.0.0 Changes** + +Changes:
+ +- Developer Improvements: + - Typescript conversion continued and now sits at ~90% + - Dev Bug Fixes: + - Deleted ts-coverage files and added folder to git.ignore so TS conversion status is properly reflected on the GitHub repository. + - Cleaned up outdated code and removed multiple unused and duplicate files, particularly those related to the now-obsolete Dark Mode functionality and some other lingering code from the v.17 migration. + - Modularity: + - Migrated large portions of RoomContainer functionality into smaller components to improve the reusability of code. + - Created more interface types for reusability to multiple parts of the applications. +- User Features: + - Collaboration Room: + - Implemented room functionality where multiple users can see and interact with the same canvas state in real time. + - Dynamically handles the host logic of the collab room, where the oldest connected client is the one serving the room's state. + - Fixed backend web socket connections with the clients, allowing full duplex connections between multiple clients and servers. + - Note: The collaboration room is in beta + - User List: + - List that displays all connected users in a particular room. + - Dynamically updates when users join or leave a room. + - Automatically updates the new host in the room to the next oldest user. + - Join/Nickname Button: + - Allows users to specify which room to join, and what name to display upon joining the room. + - The button only shows when the user is not connected to the room, requiring both fields to be filled out. + +Recommendations for Future Enhancements:
+ +- Chat functionality so users in the same room can discuss their projects. +- List of active rooms so users can simply pick one to join. Will likely be paired with a password feature for security, so only users with the proper credentials can join a particular room. +- True real-time rendering so users can see components as they're being dragged onto the canvas, rather than only when they're placed. +- Optimize performance of room state updates +- v.17 recommendations regarding the Marketplace are still undeveloped. +- Solve residual bugs. Undo & Redo buttons on the customization page not functioning as expected. Backend bugs persist as seen in the console when running the dev environment. Persistent Redux error that causes page to rerender more often than necessary. +- Resolve electron app functionality to coincide with web app functionality. +- For the state manager option in the data table there is a MuiData-menu that is not visible when clicking it and after the filter option is clicked it creates a white space in the bottom of the page. +- Expand testing coverage. Continue fixing old tests which rely on outdated dependencies, and implementing new tests. +- Continue modularizing code. Many large, unwieldy files that should be broken up into more modular components still exist. +- Continue Typescript conversion. Consider toggling noImplicitAny to find all 'any' cases that can be addressed. +- Continue cleanup of outdated and unused code and files. +- Collaboration feature still needs to be improved for scalability with AWS servers and clients for better experiences. The feature currently is limited to access with only 1 AWS cloud server. +- Future teams could look into data structures for scaling on the server side of the app to improve data transmitting and multiple server functionality. + +**Version 17.0.0 Changes** + +Changes:
+ +- Developer Improvements: + - Testing Coverage: + - Version 17 added testing for the added marketplace-related components + - Testing coverage sits at ~60% + - Typescript continued and now sits at ~80% + - Dev Bug Fixes: + - Additional logic added for edge cases in inputs for state manager (passing in non-Arrays/non-Objects as Array type and Object type). + - Fixed issue with the bottom panel not dragging or sticking to the mouse when the mouse is over the demo render iframe + - Cleaned up hundreds of lines of outdated code and archived multiple unused and duplicate files + - OAuth now linked to standalone Gmail and GitHub accounts +- User Features: + - UI updated with a modern style for a better developer experience + - Added many user feedback alerts for a better experience including alerts for when projects are published, cloned, deleted, HTML custom tags are created, context created, or custom components created. + - Built a specific buttons menu that individually displays the HTML elements, reusable components created, and join room option. + - Redesigned the state manager panel option to be readable and functional. + - Drop down menu now closes only when the user clicks outside of the menu + - Marketplace: + - Implemented a dedicated area for developers to share their projects + - Routing handled by React Router + - Projects can also be cloned to the user's account to be used and edited with the addition of a button + - Added search functionality to search by username and project name + - Included a separate section in the Saved Projects and Delete Projects modal in the Manage Project menu for cloned projects from the Marketplace + - Publish/Unpublish Button: + - Publish feature on the web app allows users to publish their saved project files into the Marketplace from the main app page + - Dynamically switches between publish/unpublish depending on whether the loaded project is in the Marketplace + +Recommendations for Future Enhancements:
+ +- Add a comment section and description section for each published project +- Consider maybe a way for users to pull individual components from one project into another +- Use localforage or other methods to store unsaved projects either on logout or accidental closure of the browser, so that when the user opens the browser again it is still there. +- Continue expanding testing coverage. Improve testing by adding additional unit tests, expanding end-to-end testing, and introducing integration testing. +- Continue quality Typescript conversion. Continue to fix type errors within component files. +- Modularize appStateSlice file. Further modularization is needed for readability and maintainability. +- Solve residual bugs. Undo & Redo buttons on customization page not functioning as expected. Backend bugs persist as seen in the console when running the dev environment. Resolve electron app functionality to coincide with web app functionality. +- Take a look at the join room functionality using web sockets in order to allow users to collaborate on the same project at the same time. +- For the state manager option in the data table there is a MuiData-menu that is not visible when clicking it and after the filter option is clicked it creates a white space in the bottom of the page. +- Continue code cleanup. Continue cleanup of outdated and unused code and files + +**Version 16.0.0 Changes** + +Changes:
+ +- Developer Improvements: + - Testing Coverage: + - Version 16 introduces end-to-end testing with Playwright and adds additional unit testing with React Testing Library. + - Testing coverage has now doubled since version 15, and now sits at just over 50% coverage. + - Transitioned away from Enzyme to maintain consistency with RTL and Jest. + - Typescript Conversion: + - Upped typescript coverage from 30% to 80%. + - Fixed multiple type errors in component files. + - Added CI pipeline for testing: + - Transitioned away from Travis CI to Github Actions for improved CI pipeline. Github Actions will now run all tests upon each pull request to dev. + - Updated OAuth and Sign In Features: + - Sign in feature now connected to the latest database version. + - Fixed bug that allowed only one OAuth user to sign in at a time. + - Github OAuth is now connected to Adam Vanek. + - Dev Bug Fixes: + - Debugged ‘worker error’ on code preview & style editor by refactoring Ace-Build components. + - Additional logic added for edge cases in inputs for context manager, state manager, and signup features. + - Cleaned up hundreds of lines of outdated code and deleted multiple unused and duplicate files + - Dependency Updates: + - All previously outdated dependencies are now updated. Time it takes for the app to bundle in dev is now cut in half. +- User Features: + - Export Button: + - Export feature on the web app now allows users to download the current project as a zip file with modularized component folder, html, and css file included. + - Export feature is now available to all users including guests. + - CSS Live Rendering: + - CSS Editor changes now rendered visually in the demo page on save. + - UI Changes: + - Fixed multiple contrast issues with white text displaying on white background in State Manger Display tab tables, state management tables, table menu dropdowns, Context Manager tables, and Context Manager display. + - Adjusted context manager interface for improved UX when creating context and saving key/value pairs. + - Fixed border styling within modals and error messages that were cutting off inputs on focus. + - Added save button to customization tab. + - Bug Fixes: + - Manage project features for registered users now successfully saves, opens, and deletes projects. + - State Manager now successfully deletes state from parent components. + - Context Manager display chart renders correctly. + - CSS Editor contents now persist after rerender. + +Recommendations for Future Enhancements:
+ +- Refactor away from MUI. MUI is very opinionated and while creating components with it is easy it leaves a lot to be desired. Dark Mode also needs to be improved as color contrast is very low. +- Continue expanding testing coverage. Improve testing by adding additional unit tests, expanding end-to-end testing, and introducing integration testing. +- Continue quality Typescript conversion. Continue to fix type errors within component files. +- Modularize appStateSlice file. Further modularization is needed for readability and maintainability. +- Solve residual bugs. Undo & Redo buttons on customization page not functioning as expected. Backend bugs persist as seen in the console when running the dev environment. Resolve electron app functionality to coincide with web app functionality. +- Continue code cleanup. Continue cleanup of outdated and unused code and files + +**Version 15.0.0 Changes** + +Changes:
+ +- Developer Improvements: + - Redux Toolkit: + - Migrated state from a combination of useReducer/useContext and Redux to only using Redux Toolkit. This is the recommended modern approach to handling large state management in this sort of application. Enhances the developer experience by enabling the use of the Redux Devtools to debug, and see state/actions in real-time. + - Dependency Updates + - New developers can easily npm install without having to use an older version of node or using --legacy-peer-deps + - Updated to modern versions to take advantage of newer features +- User Features: + - Websockets: + - Users can now join rooms to collaborate in realtime + - Tailwind CSS: + - In the customization panel users can now choose between inline CSS and Tailwind. These changes are reflected in the live code preview. + - OAuth: + - Users may now log in using OAuth which enhances security, and makes sign in a breeze. + - Deployed Website: + - Containerized and deployed a working version of the application. Instead of having to download an application users may now interact live. + +Recommendations for Future Enhancements:
+ +- Continue working on State Management. There are some changes that can be made to make the application cleaner. Right now the appStateSlice is a large file which houses a lot of the reducer functions. We believe there is a way to further modularize this to make it simpler to read, and iterate upon in the future. +- Convert to using Vite. While developing we ran into issues with webpack taking a long time to reflect changes. Vite is lightweight and enhances the developer experience. +- Expand Testing Coverage. Making a large move of state management made a lot of the testing innefective since it was based upon old ways. +- Refactor away from MUI. MUI is very opinionated and while creating components with it is easy it leaves a lot to be desired. +- Residual Bugs. While migrating state there are a few lingering bugs within the application. This process should be easier now with Redux Devtools availability, but we did not have time to go through every action and conduct thorough testing. + +**Version 14.0.0 Changes** + +Changes:
+ +- Added functionality & improvements: + - Event listeners: + - Added ability to assign event listeners to elements in the bottom customization tab + - Can name the function on the event or use the default name provided + - Updates reflected in the code preview render + - Live code preview: Bottom tool tabs code preview box updates immediately and automatically to reflect the latest changes in state + - Converted the annotation button into a delete button on the canvas elements and reusable components + - Code preview render: The formatting for generated code has been corrected for improved readability +- Major UI changes: + - Left panel: + - Only display when mouse hovered over + - When extended, floats in front of the canvas without affecting the main window formatting + - Bottom panel + - Retractable feature added + - Has internal scroll ability in the tabs + - Resized functionality is stable + - Added indicator tabs to each signifying to the user their presence + - Canvas container (upper left): + - Changed the formatting to a center column with readable size and label orientation + - Standardized the size of components and rate of growth when nesting + - Tutorial: + - Users can now reference tutorials in split-screen mode without the canvas being auto-cleared when going back and forth from the tutorial +- Bug fixes: + - Reusable component: The drag-and-drop feature for reusable components is now functioning smoothly and without bugs + +Recommendations for Future Enhancements:
+ +- Add function content in the current event listeners' function skeleton. +- The code output formatting in generateCode.ts is currently difficult to read, and could be improved for better readability. +- Currently, the project uses two sets of state management tools: useReducer/useContext and Redux. useReducer/useContext is used for handling the customization state, and Redux for managing the code preview, context manager, and dark mode reducer state. However, there seems to be some confusion around how to integrate these two tools effectively. For instance, both tools are used for managing the code preview state, and changing the useReducer/useContext state would replace the corresponding redux state. Need to clean up the logic and find a solution to solve this issue. +- Some of the files structure is not accurate (e.g., customizationPanel.tsx is in the containers folder instead of the bottom folder), need to rearrange the file hierarchy. +- Update packages and resolve package dependency issues. + +**Version 13.0.0 Changes** + +New Functionality:
+ +- Manage state locally: Users can now manage state dynamically within nested components using React Hooks within the state manager tab. +- Add/delete props: For a selected component, users can see a list of available props from the parent, add props, and delete props in case they are not - required later on. +- State/props flow: If state or props are deleted upstream, it will automatically update the state for its children components. +- Visualize state/props flow: Within the display sub-tab of the state manager tab, users can visualize an interactive tree diagram depicting the state initialized in the current component and passed down props from the parent component. + +Enhancements:
+ +- Live code preview: Live rendering of code based on any changes in the state and dragging and nesting of components. +- Next.js & Gatsby compatibility: New state manager tab is now compatible with next.js and Gatsby. +- Tutorial: Tutorial is functional and has the latest guides to navigate through the newly added state management tab. + +Deployment Updates:
+ +- Electron app is now available for Windows users. +- Web based version of the app is available on Heroku. + +Bug Fixes:
+ +- User dashboard: The dashboard works now and shows private and shared projects with the ability for users to drop comments. +- Login/logout: Users can now signup/login/logout now on both development and production environments. +- Manage Projects: Github authenticated users are now able to create and save projects. +- Customization: Use State works as expected now within HTML elements. + +What’s next:
+ +- Adding on click functionality within components. Goal: Make a fully functional tic-tac-toe app. +- Incorporating material ui into the components so that exported app has visually appealing components. +- Enabling auto save functionality when dragging and dropping components, and amending component state. +- Allowing users to click and access projects within the dashboard for review. +- Adding more integration and E2E testing. +- Fixing bugs in the heroku (web based) deployment: login/logout, GitHub oauth etc. +- Enabling google oauth in all environments. +- Packaging electron app for Linux users. +- Conversion from monolithic to micro services for better scaling in the future. + +**Version 12.0.0 Changes** + +- Context Visualizer: You can now visually see what component is consuming which context. As you click on the interactive tree, the component assigned to the context will be revealed. +- React 18: Updated to React 18 +- Export Feature: Created an exportable context file, integrated with original codebase. Ready to go code: Added boilerplate codes to components based on which contexts they are consuming. + +**A note to future contributors** + +Attempted to implement Facebook and Google OAuth via passport but as of Electron’s current version, neither of them not compatible with electron. + +**Version 11.0.0 Changes:** + +- Added Next.js functionality + - Next.js projects will generate the right code needed for exporting a Next.js application + - Link & Image elements have been added + - Link components are able to couple with a page to enable SSR + - Next Link components have a drop down menu to quickly and easily link pages + - Current canvas can be saved as a page to be coupled with a Next.js Link element + - Files are exported with the appropriate Next.js imports and structure +- Added Redux and began migrating some state over for ease of development (debugging & readability) +- Fixed bug causing electron to crash when closing the window rather than going to file > exit +- Fixed bug causing app to crash when project was changed to either Next.js or Gatsby.js +- Fixed GitHub OAuth + - added Passport.js & Passport-Github libraries for strategies which takes care of all the credential exchanges and session information + - linked electron front end to talk to backend to exchange credentials +- Fixed code preview not displaying properly +- Fixed demo render preview so that changes in the canvas appears instantly + - any links in the demo render preview can now be clicked on and it will take you to its related page +- Properties of each component now persist in the customization tab +- Fixed dark mode not syncing properly across pages + +**Version 11.0.0 Stretch Features:** + +- Move more state away from the react hook & context API and into the Redux store + - This will be very time consuming but will make implementing new features much easier + - Highly suggest you read Redux documentation on best practices before diving into this + - This will improve performance by reducing the amount of unneccessary re-render. The context API causes certain pieces of state to be needlessly coupled + - Debugging is much easier by the use of Redux dev tools which allow time travel debugging + - Code will be easier to read and thus data flow will be easier to visualize + - Don't move **everything** onto Redux. ie: Material UI uses the context API to handle theme changes +- Enable remote work similar to vscode's live share + - Tried to implement peer to peer communication via webRTC with redux swarmlog but was not successful + - Look into using websockets + - Think about security. What features needs to be implemented for secure sharing? + - Transfer actions through websockets via Redux middleware (Thunk)? +- Save project (state) onto local storage for guests +- Redesign UI to be more flexible + - Read material ui docs for best practices. + - creation panel should be redesigned. Its react component structure is too fragmented. +- Add missing Next.js features + - Image components need sizing & loading options to capitalize on Next.js' Image optimization diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b4627fdaf..c9430d234 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,125 +1,125 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Dockerfile b/Dockerfile index 5eebc20b0..27226349b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,36 @@ -# Stage 1: Build -FROM node:21.2.0-alpine as build - -# python: required dependency for node alpine, shrinks image size from 2.17GB to 1.67GB -RUN apk add --no-cache --virtual .gyp \ - python3 \ - make \ - g++ - -WORKDIR /app - -COPY package*.json ./ - -RUN npm install --no-install-recommends --fetch-retry-maxtimeout 500000 - -COPY . . - -# Stage 2: Runtime -FROM node:21.2.0-alpine as runtime - -WORKDIR /app - -COPY --from=build /app/package*.json ./ - -RUN npm install --no-install-recommends --only=production --fetch-retry-maxtimeout 500000 - -# COPY --from=build /app/.env .env -COPY --from=build /app/config.js ./config.js -COPY --from=build /app/server ./server -COPY --from=build /app/build /app - -EXPOSE 5656 - -ENV IS_DOCKER true - +# Stage 1: Build +FROM node:21.2.0-alpine as build + +# python: required dependency for node alpine, shrinks image size from 2.17GB to 1.67GB +RUN apk add --no-cache --virtual .gyp \ + python3 \ + make \ + g++ + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install --no-install-recommends --fetch-retry-maxtimeout 500000 + +COPY . . + +# Stage 2: Runtime +FROM node:21.2.0-alpine as runtime + +WORKDIR /app + +COPY --from=build /app/package*.json ./ + +RUN npm install --no-install-recommends --only=production --fetch-retry-maxtimeout 500000 + +# COPY --from=build /app/.env .env +COPY --from=build /app/config.js ./config.js +COPY --from=build /app/server ./server +COPY --from=build /app/build /app + +EXPOSE 5656 + +ENV IS_DOCKER true + CMD [ "npm", "start" ] \ No newline at end of file diff --git a/Dockerrun.aws.json b/Dockerrun.aws.json index a4a538840..3930f4966 100644 --- a/Dockerrun.aws.json +++ b/Dockerrun.aws.json @@ -1,18 +1,18 @@ -{ - "AWSEBDockerrunVersion": "1", - "Image": { - "Name": "035101486432.dkr.ecr.us-east-1.amazonaws.com/reactype-2stage:latest", - "Update": "true" - }, - "Ports": [ - { - "ContainerPort": "5656" - } - ], - "Environment": [ - { - "Name": "API_BASE_URL", - "Value": "Reactype-v19env.eba-sw2fhsbj.us-east-1.elasticbeanstalk.com" - } - ] -} +{ + "AWSEBDockerrunVersion": "1", + "Image": { + "Name": "035101486432.dkr.ecr.us-east-1.amazonaws.com/reactype-2stage:latest", + "Update": "true" + }, + "Ports": [ + { + "ContainerPort": "5656" + } + ], + "Environment": [ + { + "Name": "API_BASE_URL", + "Value": "Reactype-v19env.eba-sw2fhsbj.us-east-1.elasticbeanstalk.com" + } + ] +} diff --git a/LICENSE.md b/LICENSE.md index 41ccbcd37..177a6267d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,9 +1,9 @@ -MIT License - -Copyright (c) 2022 ReacType - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MIT License + +Copyright (c) 2022 ReacType + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 5ce612d06..b9012244c 100644 --- a/README.md +++ b/README.md @@ -1,172 +1,172 @@ - - - - - - - -
- -[![StarShield][stars]][stars-url] -[![ContributorShield][contributors]][contributors-url] -[![ForksShield][forks]][forks-url] -![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) -![Version: 20.0.0](https://img.shields.io/badge/version-20.0.0-orange) - -
- -

- -

- -![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) -![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) -![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) -![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) -![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) -![Redux](https://img.shields.io/badge/redux-%23593d88.svg?style=for-the-badge&logo=redux&logoColor=white) -![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) -![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) -![Babel](https://img.shields.io/badge/Babel-F9DC3e?style=for-the-badge&logo=babel&logoColor=black) -![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) -![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white) -![Electron.js](https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white) -![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) -![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) -![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) -![Vite](https://img.shields.io/badge/Vite-orange?logo=vite&style=for-the-badge) - -

ReacType

- -**ReacType** is a React prototyping tool that allows users _visualize_ their application architecture dynamically, employing an interactive drop and drag display with real-time component code preview and a collaboration room that features live video and chat functionality. Generated code can be exported as a **React** app for developers employing React component architecture alongside the comprehensive type-checking of **TypeScript**. In other words, **you can draw prototypes and export React / TypeScript code!** - -

- -

- -Visit [reactype.dev](https://reactype.dev) to learn more about the product. - -Follow [@ReacType](https://twitter.com/reactype) on Twitter for important announcements. - -## Changes with Version 20.0.0 - -- **Collaboration Rooms**: Official launch of v2 collaboration rooms - Now featuring a secure live collaborative chat room with video and cursor tracking functionality! -- **UI Updates**: The UI now features a new logo, zoom and scroll functionality to the canvas, and numerous updates to styling to reflect a more modern and user friendly experience. -- **DX Updates**: Migrated from WebPack to Vite, drastically reducing HMR time. Now deployed via Heroku instead of AWS. -- **Typescript Conversion**: Typescript coverage is at 95%. -- **Cleanup**: Removed unused code, fixed bugs, and made major performance improvements. -- **And more:** See the [change log](https://github.com/open-source-labs/ReacType/blob/master/CHANGE_LOG.md) for more details on what was changed from the previous versions, as well as plans for upcoming features! - -

- -

- -## Preview - -Get a glimpse of how ReacType works! - -

- -

- - - -## File Structure of ReacType Version 20.0.0 - -Here is the main file structure: - -

- -

- -Given to us courtesy of our friends over at React Relay - -## Run ReacType using CLI - -- **Fork** and **Clone** Repository. -- Open the project directory. -- Install dependencies. - -```bash -npm install -``` - -- To run the production build - -```bash -npm run prod -``` - -- To run tests - -```bash -npm run test -``` - -- To run the development build - -```bash -npm run dev -``` - -- Note that DEV_PORT, NODE_ENV flag (=production or development) and VIDEOSDK token are needed in the .env file. -- Please note that the development build is not connected to the production server. `npm run dev` should spin up the development server from the server folder of this repo. For additional information, the readme is [here](https://github.com/open-source-labs/ReacType/blob/master/server/README.md). Alternatively, you can select "Continue as guest" on the login page of the app, which will not use any features that rely on the server (authentication and saving project data.) - -- To run the development build of electron app - -```bash -npm run dev -npm run electron-dev -``` - -## Run Exported App - -- Open exported project directory -- Install dependencies - -```bash -npm install -``` - -- Build the app - -```bash -npm run build -``` - -- Start an instance - -```bash -npm run start -``` - -- Open browser and navigate to localhost at specified port - -## Stack - -Typescript, React.js, Redux Toolkit, Javascript, ESM, Node.js (Express), HTML, CSS, MUI, GraphQL, Next.js, Gatsby.js, Electron, NoSQL, Webpack, TDD (Jest, React Testing Library, Playwright), OAuth 2.0, Websocket, SocketIO, Continuous Integration (Github Actions), Docker, AWS (ECR, Elastic Beanstalk), Ace Editor, Google Charts, React DnD, Vite - -## Contributions - -Here is the up to date [list](https://github.com/open-source-labs/ReacType/blob/master/contributors.md) of all co-developers of this product. - -## License - -This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/team-reactype/ReacType/blob/development/LICENSE.md) file for details. - -[stars]: https://img.shields.io/github/stars/open-source-labs/ReacType -[stars-url]: https://github.com/open-source-labs/ReacType/stargazers -[forks]: https://img.shields.io/github/forks/open-source-labs/ReacType -[forks-url]: https://github.com/open-source-labs/ReacType/network/members -[contributors]: https://img.shields.io/github/contributors/open-source-labs/ReacType -[contributors-url]: https://github.com/open-source-labs/ReacType/graphs/contributors + + + + + + + +
+ +[![StarShield][stars]][stars-url] +[![ContributorShield][contributors]][contributors-url] +[![ForksShield][forks]][forks-url] +![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) +![Version: 20.0.0](https://img.shields.io/badge/version-20.0.0-orange) + +
+ +

+ +

+ +![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) +![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) +![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) +![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) +![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) +![Redux](https://img.shields.io/badge/redux-%23593d88.svg?style=for-the-badge&logo=redux&logoColor=white) +![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) +![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) +![Babel](https://img.shields.io/badge/Babel-F9DC3e?style=for-the-badge&logo=babel&logoColor=black) +![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) +![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white) +![Electron.js](https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white) +![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) +![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) +![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) +![Vite](https://img.shields.io/badge/Vite-orange?logo=vite&style=for-the-badge) + +

ReacType

+ +**ReacType** is a React prototyping tool that allows users _visualize_ their application architecture dynamically, employing an interactive drop and drag display with real-time component code preview and a collaboration room that features live video and chat functionality. Generated code can be exported as a **React** app for developers employing React component architecture alongside the comprehensive type-checking of **TypeScript**. In other words, **you can draw prototypes and export React / TypeScript code!** + +

+ +

+ +Visit [reactype.dev](https://reactype.dev) to learn more about the product. + +Follow [@ReacType](https://twitter.com/reactype) on Twitter for important announcements. + +## Changes with Version 20.0.0 + +- **Collaboration Rooms**: Official launch of v2 collaboration rooms - Now featuring a secure live collaborative chat room with video and cursor tracking functionality! +- **UI Updates**: The UI now features a new logo, zoom and scroll functionality to the canvas, and numerous updates to styling to reflect a more modern and user friendly experience. +- **DX Updates**: Migrated from WebPack to Vite, drastically reducing HMR time. Now deployed via Heroku instead of AWS. +- **Typescript Conversion**: Typescript coverage is at 95%. +- **Cleanup**: Removed unused code, fixed bugs, and made major performance improvements. +- **And more:** See the [change log](https://github.com/open-source-labs/ReacType/blob/master/CHANGE_LOG.md) for more details on what was changed from the previous versions, as well as plans for upcoming features! + +

+ +

+ +## Preview + +Get a glimpse of how ReacType works! + +

+ +

+ + + +## File Structure of ReacType Version 20.0.0 + +Here is the main file structure: + +

+ +

+ +Given to us courtesy of our friends over at React Relay + +## Run ReacType using CLI + +- **Fork** and **Clone** Repository. +- Open the project directory. +- Install dependencies. + +```bash +npm install +``` + +- To run the production build + +```bash +npm run prod +``` + +- To run tests + +```bash +npm run test +``` + +- To run the development build + +```bash +npm run dev +``` + +- Note that DEV_PORT, NODE_ENV flag (=production or development) and VIDEOSDK token are needed in the .env file. +- Please note that the development build is not connected to the production server. `npm run dev` should spin up the development server from the server folder of this repo. For additional information, the readme is [here](https://github.com/open-source-labs/ReacType/blob/master/server/README.md). Alternatively, you can select "Continue as guest" on the login page of the app, which will not use any features that rely on the server (authentication and saving project data.) + +- To run the development build of electron app + +```bash +npm run dev +npm run electron-dev +``` + +## Run Exported App + +- Open exported project directory +- Install dependencies + +```bash +npm install +``` + +- Build the app + +```bash +npm run build +``` + +- Start an instance + +```bash +npm run start +``` + +- Open browser and navigate to localhost at specified port + +## Stack + +Typescript, React.js, Redux Toolkit, Javascript, ESM, Node.js (Express), HTML, CSS, MUI, GraphQL, Next.js, Gatsby.js, Electron, NoSQL, Webpack, TDD (Jest, React Testing Library, Playwright), OAuth 2.0, Websocket, SocketIO, Continuous Integration (Github Actions), Docker, AWS (ECR, Elastic Beanstalk), Ace Editor, Google Charts, React DnD, Vite + +## Contributions + +Here is the up to date [list](https://github.com/open-source-labs/ReacType/blob/master/contributors.md) of all co-developers of this product. + +## License + +This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/team-reactype/ReacType/blob/development/LICENSE.md) file for details. + +[stars]: https://img.shields.io/github/stars/open-source-labs/ReacType +[stars-url]: https://github.com/open-source-labs/ReacType/stargazers +[forks]: https://img.shields.io/github/forks/open-source-labs/ReacType +[forks-url]: https://github.com/open-source-labs/ReacType/network/members +[contributors]: https://img.shields.io/github/contributors/open-source-labs/ReacType +[contributors-url]: https://github.com/open-source-labs/ReacType/graphs/contributors diff --git a/__tests__/BottomTabs.test.tsx b/__tests__/BottomTabs.test.tsx index e7d55068e..31145f71d 100644 --- a/__tests__/BottomTabs.test.tsx +++ b/__tests__/BottomTabs.test.tsx @@ -1,233 +1,233 @@ -import '@testing-library/jest-dom'; - -import { - fireEvent, - render, - screen, - waitFor, - within -} from '@testing-library/react'; - -import BottomTabs from '../app/src/components/bottom/BottomTabs'; -import { BrowserRouter } from 'react-router-dom'; -import ComponentPanel from '../app/src/components/right/ComponentPanel'; -import ContextManager from '../app/src/components/ContextAPIManager/ContextManager'; -import CustomizationPanel from '../app/src/containers/CustomizationPanel'; -import { DndProvider } from 'react-dnd'; -import DragDropPanel from '../app/src/components/left/DragDropPanel'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import HTMLPanel from '../app/src/components/left/HTMLPanel'; -import MainContainer from '../app/src/containers/MainContainer'; -import { Provider } from 'react-redux'; -import React from 'react'; -import StateManager from '../app/src/components/StateManagement/StateManagement'; -import store from '../app/src/redux/store'; - -describe('Bottom Panel Render Test', () => { - test('should render all six tabs', () => { - render( - - - - ); - expect(screen.getAllByRole('tab')).toHaveLength(6); - // expect(screen.getByText('Code Preview')).toBeInTheDocument(); - expect(screen.getByText('Component Tree')).toBeInTheDocument(); - expect(screen.getByText('Creation Panel')).toBeInTheDocument(); - expect(screen.getByText('Customization')).toBeInTheDocument(); - expect(screen.getByText('CSS Editor')).toBeInTheDocument(); - expect(screen.getByText('Context Manager')).toBeInTheDocument(); - expect(screen.getByText('State Manager')).toBeInTheDocument(); - }); -}); - -describe('Creation Panel', () => { - test('should invalidate empty field in New Component name', async () => { - render( - - - - ); - - fireEvent.click(screen.getByText('Create')); - - await waitFor(() => { - expect( - screen.getByText('Component name cannot be blank.') - ).toBeInTheDocument(); - }); - }); - - test('should invalidate New Component name containing symbols', async () => { - render( - - - - ); - - fireEvent.change(screen.getByLabelText('Name:'), { - target: { - value: '!@#' - } - }); - - fireEvent.click(screen.getByText('Create')); - - await waitFor(() => { - expect( - screen.getByText('Component name must start with a letter.') - ).toBeInTheDocument(); - }); - }); - - test('should invalidate empty field in HTML Tag tag', async () => { - render( - - - - ); - - fireEvent.click(screen.getByText('Add Element')); - - await waitFor(() => { - expect(screen.getAllByText('* Input cannot be blank. *')).toHaveLength(2); - }); - }); - - test('should invalidate HTML Element name containing symbols', async () => { - render( - - - - ); - - fireEvent.change(screen.getByLabelText('Element Name:'), { - target: { - value: '!@#' - } - }); - - fireEvent.change(screen.getByLabelText('Tag:'), { - target: { - value: '!@#' - } - }); - - fireEvent.click(screen.getByText('Add Element')); - - await waitFor(() => { - expect( - screen.getAllByText('* Input must start with a letter. *') - ).toHaveLength(2); - }); - }); -}); - -describe('Context Manager', () => { - test('should render Create/Edit, Assign, and Display tabs', () => { - render( - - - - ); - expect(screen.getAllByRole('tab')).toHaveLength(3); - }); - test('Create/Edit Tab should contain all buttons, inputs field, and a data table', () => { - render( - - - - ); - expect(screen.getAllByRole('textbox')).toHaveLength(3); - expect(screen.getAllByRole('button')).toHaveLength(4); - expect(screen.getByText('Context Name')).toBeInTheDocument(); - expect(screen.getByRole('table')).toBeInTheDocument(); - }); - test('Assign Tab should contain all buttons and input fields', () => { - render( - - - - ); - - fireEvent.click(screen.getByText('Assign')); - expect(screen.getByText('Contexts Consumed')).toBeInTheDocument(); - const dropdown = screen.getByLabelText('Select Component'); - expect(dropdown).toBeInTheDocument(); - expect(screen.getAllByRole('button')).toHaveLength(1); - expect(screen.getAllByRole('combobox')).toHaveLength(2); - expect(screen.getAllByRole('table')).toHaveLength(2); - }); -}); - -describe('State Manager', () => { - test('Should render all containers', () => { - render( - - - - ); - expect(screen.getAllByRole('heading')).toHaveLength(4); - expect(screen.getAllByRole('textbox')).toHaveLength(2); - expect(screen.getAllByRole('grid')).toHaveLength(3); - expect(screen.getAllByRole('columnheader')).toHaveLength(9); - }); - - test('Display tab should render correct elements', () => { - render( - - - - ); - fireEvent.click(screen.getByText('Display')); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect( - screen.getByText('State Initialized in Current Component:') - ).toBeInTheDocument(); - }); -}); - -describe('Customization Panel', () => { - test('Should render customization container with no elements in Canvas', () => { - render( - - - - - - ); - expect(screen.getByText('Parent Component:')).toBeInTheDocument(); - expect(screen.getByText('App')).toBeInTheDocument(); - expect( - screen.getByText( - 'Drag and drop an html element (or focus one) to see what happens!' - ) - ).toBeInTheDocument(); - }); - test('Should render all buttons and inputs when Canvas has element', () => { - render( - - - - - - - - - - ); - const drop = screen.getByTestId('drop'); - const div = screen.getAllByText('Div')[0]; - expect(drop).toBeInTheDocument(); - expect(div).toBeInTheDocument(); - fireEvent.dragStart(div); - fireEvent.dragEnter(drop); - fireEvent.dragOver(drop); - fireEvent.drop(drop); - //check if customization panel elements are rendering correctly - const panel = screen.getByTestId('customization'); - expect(within(panel).getAllByRole('textbox')).toHaveLength(4); - // check dropdowns - expect(within(panel).getAllByRole('button')).toHaveLength(12); - }); -}); +import '@testing-library/jest-dom'; + +import { + fireEvent, + render, + screen, + waitFor, + within +} from '@testing-library/react'; + +import BottomTabs from '../app/src/components/bottom/BottomTabs'; +import { BrowserRouter } from 'react-router-dom'; +import ComponentPanel from '../app/src/components/right/ComponentPanel'; +import ContextManager from '../app/src/components/ContextAPIManager/ContextManager'; +import CustomizationPanel from '../app/src/containers/CustomizationPanel'; +import { DndProvider } from 'react-dnd'; +import DragDropPanel from '../app/src/components/left/DragDropPanel'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import HTMLPanel from '../app/src/components/left/HTMLPanel'; +import MainContainer from '../app/src/containers/MainContainer'; +import { Provider } from 'react-redux'; +import React from 'react'; +import StateManager from '../app/src/components/StateManagement/StateManagement'; +import store from '../app/src/redux/store'; + +describe('Bottom Panel Render Test', () => { + test('should render all six tabs', () => { + render( + + + + ); + expect(screen.getAllByRole('tab')).toHaveLength(6); + // expect(screen.getByText('Code Preview')).toBeInTheDocument(); + expect(screen.getByText('Component Tree')).toBeInTheDocument(); + expect(screen.getByText('Creation Panel')).toBeInTheDocument(); + expect(screen.getByText('Customization')).toBeInTheDocument(); + expect(screen.getByText('CSS Editor')).toBeInTheDocument(); + expect(screen.getByText('Context Manager')).toBeInTheDocument(); + expect(screen.getByText('State Manager')).toBeInTheDocument(); + }); +}); + +describe('Creation Panel', () => { + test('should invalidate empty field in New Component name', async () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Create')); + + await waitFor(() => { + expect( + screen.getByText('Component name cannot be blank.') + ).toBeInTheDocument(); + }); + }); + + test('should invalidate New Component name containing symbols', async () => { + render( + + + + ); + + fireEvent.change(screen.getByLabelText('Name:'), { + target: { + value: '!@#' + } + }); + + fireEvent.click(screen.getByText('Create')); + + await waitFor(() => { + expect( + screen.getByText('Component name must start with a letter.') + ).toBeInTheDocument(); + }); + }); + + test('should invalidate empty field in HTML Tag tag', async () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Add Element')); + + await waitFor(() => { + expect(screen.getAllByText('* Input cannot be blank. *')).toHaveLength(2); + }); + }); + + test('should invalidate HTML Element name containing symbols', async () => { + render( + + + + ); + + fireEvent.change(screen.getByLabelText('Element Name:'), { + target: { + value: '!@#' + } + }); + + fireEvent.change(screen.getByLabelText('Tag:'), { + target: { + value: '!@#' + } + }); + + fireEvent.click(screen.getByText('Add Element')); + + await waitFor(() => { + expect( + screen.getAllByText('* Input must start with a letter. *') + ).toHaveLength(2); + }); + }); +}); + +describe('Context Manager', () => { + test('should render Create/Edit, Assign, and Display tabs', () => { + render( + + + + ); + expect(screen.getAllByRole('tab')).toHaveLength(3); + }); + test('Create/Edit Tab should contain all buttons, inputs field, and a data table', () => { + render( + + + + ); + expect(screen.getAllByRole('textbox')).toHaveLength(3); + expect(screen.getAllByRole('button')).toHaveLength(4); + expect(screen.getByText('Context Name')).toBeInTheDocument(); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + test('Assign Tab should contain all buttons and input fields', () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Assign')); + expect(screen.getByText('Contexts Consumed')).toBeInTheDocument(); + const dropdown = screen.getByLabelText('Select Component'); + expect(dropdown).toBeInTheDocument(); + expect(screen.getAllByRole('button')).toHaveLength(1); + expect(screen.getAllByRole('combobox')).toHaveLength(2); + expect(screen.getAllByRole('table')).toHaveLength(2); + }); +}); + +describe('State Manager', () => { + test('Should render all containers', () => { + render( + + + + ); + expect(screen.getAllByRole('heading')).toHaveLength(4); + expect(screen.getAllByRole('textbox')).toHaveLength(2); + expect(screen.getAllByRole('grid')).toHaveLength(3); + expect(screen.getAllByRole('columnheader')).toHaveLength(9); + }); + + test('Display tab should render correct elements', () => { + render( + + + + ); + fireEvent.click(screen.getByText('Display')); + expect(screen.getByRole('table')).toBeInTheDocument(); + expect( + screen.getByText('State Initialized in Current Component:') + ).toBeInTheDocument(); + }); +}); + +describe('Customization Panel', () => { + test('Should render customization container with no elements in Canvas', () => { + render( + + + + + + ); + expect(screen.getByText('Parent Component:')).toBeInTheDocument(); + expect(screen.getByText('App')).toBeInTheDocument(); + expect( + screen.getByText( + 'Drag and drop an html element (or focus one) to see what happens!' + ) + ).toBeInTheDocument(); + }); + test('Should render all buttons and inputs when Canvas has element', () => { + render( + + + + + + + + + + ); + const drop = screen.getByTestId('drop'); + const div = screen.getAllByText('Div')[0]; + expect(drop).toBeInTheDocument(); + expect(div).toBeInTheDocument(); + fireEvent.dragStart(div); + fireEvent.dragEnter(drop); + fireEvent.dragOver(drop); + fireEvent.drop(drop); + //check if customization panel elements are rendering correctly + const panel = screen.getByTestId('customization'); + expect(within(panel).getAllByRole('textbox')).toHaveLength(4); + // check dropdowns + expect(within(panel).getAllByRole('button')).toHaveLength(12); + }); +}); diff --git a/__tests__/DragAndDrop.test.tsx b/__tests__/DragAndDrop.test.tsx index 8241523ae..3b82c001a 100644 --- a/__tests__/DragAndDrop.test.tsx +++ b/__tests__/DragAndDrop.test.tsx @@ -1,78 +1,78 @@ -import '@testing-library/jest-dom'; - -import { fireEvent, render, screen } from '@testing-library/react'; - -import ComponentDrag from '../app/src/components/left/ComponentDrag'; -import { DndProvider } from 'react-dnd'; -import DragDropPanel from '../app/src/components/left/DragDropPanel'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import MainContainer from '../app/src/containers/MainContainer'; -import { Provider } from 'react-redux'; -import React from 'react'; -import store from '../app/src/redux/store'; -import { within } from '@testing-library/react'; - -function TestContext(component) { - return ( - - {component} - - ); -} - -describe('Drag and Drop Side Panel', () => { - test('Renders all HTML Element choices', () => { - render(TestContext()); - expect(screen.getByText('Div')).toBeInTheDocument(); - expect(screen.getByText('Img')).toBeInTheDocument(); - expect(screen.getByText('Form')).toBeInTheDocument(); - expect(screen.getByText('Button')).toBeInTheDocument(); - expect(screen.getByText('Link')).toBeInTheDocument(); - expect(screen.getByText('Paragraph')).toBeInTheDocument(); - expect(screen.getByText('Header 1')).toBeInTheDocument(); - expect(screen.getByText('Header 2')).toBeInTheDocument(); - expect(screen.getByText('Span')).toBeInTheDocument(); - expect(screen.getByText('Input')).toBeInTheDocument(); - expect(screen.getByText('Label')).toBeInTheDocument(); - expect(screen.getByText('Ordered List')).toBeInTheDocument(); - expect(screen.getByText('Unordered List')).toBeInTheDocument(); - expect(screen.getByText('Menu')).toBeInTheDocument(); - expect(screen.getByText('List')).toBeInTheDocument(); - expect(screen.queryByText('separator')).toBe(null); - }); - - test('Renders all React Router Component choices', () => { - render(TestContext()); - - expect(screen.getByText('Switch')).toBeInTheDocument(); - expect(screen.getByText('Route')).toBeInTheDocument(); - expect(screen.getByText('LinkTo')).toBeInTheDocument(); - }); - - test.skip('Should render Roots Components and Reusbale components', () => { - render(TestContext()); - - expect(screen.getByText('Root Components')).toBeInTheDocument(); - expect(screen.getByText('Reusable Components')).toBeInTheDocument(); - }); - - test('test drag and drop', () => { - render( - TestContext( - <> - - - - ) - ); - const drop = screen.getByTestId('drop'); - const div = screen.getByText('Div'); - expect(drop).toBeInTheDocument(); - expect(div).toBeInTheDocument(); - fireEvent.dragStart(div); - fireEvent.dragEnter(drop); - fireEvent.dragOver(drop); - fireEvent.drop(drop); - expect(within(drop).getByText('div')).toBeInTheDocument(); - }); -}); +import '@testing-library/jest-dom'; + +import { fireEvent, render, screen } from '@testing-library/react'; + +import ComponentDrag from '../app/src/components/left/ComponentDrag'; +import { DndProvider } from 'react-dnd'; +import DragDropPanel from '../app/src/components/left/DragDropPanel'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import MainContainer from '../app/src/containers/MainContainer'; +import { Provider } from 'react-redux'; +import React from 'react'; +import store from '../app/src/redux/store'; +import { within } from '@testing-library/react'; + +function TestContext(component) { + return ( + + {component} + + ); +} + +describe('Drag and Drop Side Panel', () => { + test('Renders all HTML Element choices', () => { + render(TestContext()); + expect(screen.getByText('Div')).toBeInTheDocument(); + expect(screen.getByText('Img')).toBeInTheDocument(); + expect(screen.getByText('Form')).toBeInTheDocument(); + expect(screen.getByText('Button')).toBeInTheDocument(); + expect(screen.getByText('Link')).toBeInTheDocument(); + expect(screen.getByText('Paragraph')).toBeInTheDocument(); + expect(screen.getByText('Header 1')).toBeInTheDocument(); + expect(screen.getByText('Header 2')).toBeInTheDocument(); + expect(screen.getByText('Span')).toBeInTheDocument(); + expect(screen.getByText('Input')).toBeInTheDocument(); + expect(screen.getByText('Label')).toBeInTheDocument(); + expect(screen.getByText('Ordered List')).toBeInTheDocument(); + expect(screen.getByText('Unordered List')).toBeInTheDocument(); + expect(screen.getByText('Menu')).toBeInTheDocument(); + expect(screen.getByText('List')).toBeInTheDocument(); + expect(screen.queryByText('separator')).toBe(null); + }); + + test('Renders all React Router Component choices', () => { + render(TestContext()); + + expect(screen.getByText('Switch')).toBeInTheDocument(); + expect(screen.getByText('Route')).toBeInTheDocument(); + expect(screen.getByText('LinkTo')).toBeInTheDocument(); + }); + + test.skip('Should render Roots Components and Reusbale components', () => { + render(TestContext()); + + expect(screen.getByText('Root Components')).toBeInTheDocument(); + expect(screen.getByText('Reusable Components')).toBeInTheDocument(); + }); + + test('test drag and drop', () => { + render( + TestContext( + <> + + + + ) + ); + const drop = screen.getByTestId('drop'); + const div = screen.getByText('Div'); + expect(drop).toBeInTheDocument(); + expect(div).toBeInTheDocument(); + fireEvent.dragStart(div); + fireEvent.dragEnter(drop); + fireEvent.dragOver(drop); + fireEvent.drop(drop); + expect(within(drop).getByText('div')).toBeInTheDocument(); + }); +}); diff --git a/__tests__/NavBar.test.tsx b/__tests__/NavBar.test.tsx index 7429aa42a..7fc02d706 100644 --- a/__tests__/NavBar.test.tsx +++ b/__tests__/NavBar.test.tsx @@ -1,294 +1,294 @@ -import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { MemoryRouter } from 'react-router-dom'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import NavBar from '../app/src/components/top/NavBar'; -import * as projectFunctions from '../app/src/helperFunctions/projectGetSaveDel'; -import { Provider } from 'react-redux'; -import { act } from 'react-dom/test-utils'; -import { configureStore } from '@reduxjs/toolkit'; -import rootReducer from '../app/src/redux/reducers/rootReducer'; -import { initialState as appStateInitialState } from '../app/src/redux/reducers/slice/appStateSlice'; - -// Mock the non-serializable HTMLTypes -const mockHTMLTypes = [ - { - id: 11, - tag: 'div', - name: 'Div', - style: {}, - placeHolderShort: 'div', - placeHolderLong: '', - framework: 'reactClassic', - nestable: true, - }, - { - id: 1000, - tag: 'separator', - name: 'separator', - style: { border: 'none' }, - placeHolderShort: '', - placeHolderLong: '', - framework: '', - nestable: true, - }, - { - id: 1, - tag: 'img', - name: 'Img', - style: {}, - placeHolderShort: 'image', - placeHolderLong: '', - framework: 'reactClassic', - nestable: false, - }, -]; - -// Mocking the theme -const theme = createTheme({ - spacing: (value) => `${value}px`, -}); - -// Mocking the logo -jest.mock('../../public/icons/win/logo.png', () => 'dummy-image-url'); - -// Grabbing publish and unpublish functions -jest.mock('../app/src/helperFunctions/projectGetSaveDel', () => ({ - publishProject: jest.fn(), - unpublishProject: jest.fn(), -})); - -//mock the file saver library -jest.mock('file-saver', () => ({ - ...jest.requireActual('file-saver'), - saveAs: jest.fn(), -})); - -// const originalError = console.error; -// beforeAll(() => { -// console.error = jest.fn(); -// }); - -afterAll(() => { - jest.resetAllMocks(); -}); - -// Mocking the render -const renderNavBar = (store) => { - return render( - - - - - - - - ); -}; - -describe('NavBar Component', () => { - it('handles publish correctly with saved project', async () => { - const publishProjectMock = jest.spyOn(projectFunctions, 'publishProject'); - publishProjectMock.mockResolvedValueOnce({ - _id: 'mockedId', - name: 'Mocked Project', - published: true, - }); - - const store = configureStore({ - reducer: rootReducer, - preloadedState: { - appState: { - ...appStateInitialState, - isLoggedIn: true, - name: 'Mock Project Name', - HTMLTypes: mockHTMLTypes, - }, - }, - }); - - console.log('Before rendering NavBar'); - - const { getByText } = renderNavBar(store); - - console.log('After rendering NavBar'); - - await act(async () => { - const publishButton = getByText('Publish'); - fireEvent.click(publishButton); - }); - }); - - it('handles publish correctly with new project', async () => { - const publishProjectMock = jest.spyOn(projectFunctions, 'publishProject'); - publishProjectMock.mockResolvedValueOnce({ - _id: 'mockedId', - name: 'My Project', - published: true, - }); - - const store = configureStore({ - reducer: rootReducer, - preloadedState: { - appState: { - ...appStateInitialState, - isLoggedIn: true, - name: '', - HTMLTypes: mockHTMLTypes, - }, - }, - }); - - console.log('Before rendering NavBar'); - - const { getByText, queryByText, getByTestId, queryByTestId } = renderNavBar(store); - - console.log('After rendering NavBar'); - - await act(async () => { - // Check if the "Publish" button is present - const publishButton = queryByText('Publish'); - - if (publishButton) { - fireEvent.click(publishButton); - } else { - // If "Publish" button is not found, look for the "Unpublish" button - const unpublishButton = getByText('Unpublish'); - fireEvent.click(unpublishButton); - } - - // Check if the modal for a new project is displayed - const projectNameInput = queryByTestId('project-name-input'); - - if (projectNameInput) { - // entering a project name in the modal - fireEvent.change(projectNameInput, { target: { value: 'My Project' } }); - } - }); - }); - - it('handles unpublish correctly', async () => { - const unpublishProjectMock = jest.spyOn(projectFunctions, 'unpublishProject'); - unpublishProjectMock.mockResolvedValueOnce({ - _id: 'mockedId', - name: 'Mocked Project', - published: false, - }); - - const store = configureStore({ - reducer: rootReducer, - preloadedState: { - appState: { - ...appStateInitialState, - isLoggedIn: true, - name: 'Mock Project Name', - HTMLTypes: mockHTMLTypes, - }, - }, - }); - - console.log('Before rendering NavBar'); - - const { queryByText } = renderNavBar(store); - - console.log('After rendering NavBar'); - - // Find the "Publish" or "Unpublish" button based on the project's publish state - const publishButton = queryByText('Publish'); - const unpublishButton = queryByText('Unpublish'); - - if (publishButton) { - fireEvent.click(publishButton); - } else if (unpublishButton) { - fireEvent.click(unpublishButton); - } - }); - - it('handles export correctly', async () => { - const store = configureStore({ - reducer: rootReducer, - preloadedState: { - appState: { - ...appStateInitialState, - isLoggedIn: true, - name: 'Mock Project Name', - HTMLTypes: mockHTMLTypes, - }, - }, - }); - - console.log('Before rendering NavBar'); - - const { getByText } = renderNavBar(store); - - console.log('After rendering NavBar'); - - // Find and click the export button - const exportButton = getByText('< > Export'); - fireEvent.click(exportButton); - - // Check if the modal for export options is displayed - await waitFor(() => { - const exportModal = getByText('Click to download in zip file:'); - expect(exportModal).toBeInTheDocument(); - }); - - // Simulate clicking the export components - const exportComponentsOption = getByText('Export components'); - fireEvent.click(exportComponentsOption); - - }); - - it('handles dropdown menu correctly', async () => { - const store = configureStore({ - reducer: rootReducer, - preloadedState: { - appState: { - ...appStateInitialState, - }, - }, - }); - - console.log('Before rendering NavBar'); - - const { getByTestId, getByText } = render( - - - - - - - - ); - - console.log('After rendering NavBar'); - - await act(async () => { - - const dropdownMenu = getByTestId('navDropDown'); - expect(dropdownMenu).toHaveClass('hideNavDropDown'); - - - const moreVertButton = getByTestId('more-vert-button'); - fireEvent.click(moreVertButton); - - - expect(dropdownMenu).toHaveClass('hideNavDropDown'); - - - const clearCanvasMenuItem = getByText('Clear Canvas'); - fireEvent.click(clearCanvasMenuItem); - expect(dropdownMenu).toHaveClass('hideNavDropDown'); - - - const marketplaceMenuItem = getByText('Marketplace'); - fireEvent.click(marketplaceMenuItem); - expect(dropdownMenu).toHaveClass('hideNavDropDown'); - - fireEvent.click(moreVertButton); - - expect(dropdownMenu).toHaveClass('hideNavDropDown'); - }); - }); -}) +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import NavBar from '../app/src/components/top/NavBar'; +import * as projectFunctions from '../app/src/helperFunctions/projectGetSaveDel'; +import { Provider } from 'react-redux'; +import { act } from 'react-dom/test-utils'; +import { configureStore } from '@reduxjs/toolkit'; +import rootReducer from '../app/src/redux/reducers/rootReducer'; +import { initialState as appStateInitialState } from '../app/src/redux/reducers/slice/appStateSlice'; + +// Mock the non-serializable HTMLTypes +const mockHTMLTypes = [ + { + id: 11, + tag: 'div', + name: 'Div', + style: {}, + placeHolderShort: 'div', + placeHolderLong: '', + framework: 'reactClassic', + nestable: true, + }, + { + id: 1000, + tag: 'separator', + name: 'separator', + style: { border: 'none' }, + placeHolderShort: '', + placeHolderLong: '', + framework: '', + nestable: true, + }, + { + id: 1, + tag: 'img', + name: 'Img', + style: {}, + placeHolderShort: 'image', + placeHolderLong: '', + framework: 'reactClassic', + nestable: false, + }, +]; + +// Mocking the theme +const theme = createTheme({ + spacing: (value) => `${value}px`, +}); + +// Mocking the logo +jest.mock('../../public/icons/win/logo.png', () => 'dummy-image-url'); + +// Grabbing publish and unpublish functions +jest.mock('../app/src/helperFunctions/projectGetSaveDel', () => ({ + publishProject: jest.fn(), + unpublishProject: jest.fn(), +})); + +//mock the file saver library +jest.mock('file-saver', () => ({ + ...jest.requireActual('file-saver'), + saveAs: jest.fn(), +})); + +// const originalError = console.error; +// beforeAll(() => { +// console.error = jest.fn(); +// }); + +afterAll(() => { + jest.resetAllMocks(); +}); + +// Mocking the render +const renderNavBar = (store) => { + return render( + + + + + + + + ); +}; + +describe('NavBar Component', () => { + it('handles publish correctly with saved project', async () => { + const publishProjectMock = jest.spyOn(projectFunctions, 'publishProject'); + publishProjectMock.mockResolvedValueOnce({ + _id: 'mockedId', + name: 'Mocked Project', + published: true, + }); + + const store = configureStore({ + reducer: rootReducer, + preloadedState: { + appState: { + ...appStateInitialState, + isLoggedIn: true, + name: 'Mock Project Name', + HTMLTypes: mockHTMLTypes, + }, + }, + }); + + console.log('Before rendering NavBar'); + + const { getByText } = renderNavBar(store); + + console.log('After rendering NavBar'); + + await act(async () => { + const publishButton = getByText('Publish'); + fireEvent.click(publishButton); + }); + }); + + it('handles publish correctly with new project', async () => { + const publishProjectMock = jest.spyOn(projectFunctions, 'publishProject'); + publishProjectMock.mockResolvedValueOnce({ + _id: 'mockedId', + name: 'My Project', + published: true, + }); + + const store = configureStore({ + reducer: rootReducer, + preloadedState: { + appState: { + ...appStateInitialState, + isLoggedIn: true, + name: '', + HTMLTypes: mockHTMLTypes, + }, + }, + }); + + console.log('Before rendering NavBar'); + + const { getByText, queryByText, getByTestId, queryByTestId } = renderNavBar(store); + + console.log('After rendering NavBar'); + + await act(async () => { + // Check if the "Publish" button is present + const publishButton = queryByText('Publish'); + + if (publishButton) { + fireEvent.click(publishButton); + } else { + // If "Publish" button is not found, look for the "Unpublish" button + const unpublishButton = getByText('Unpublish'); + fireEvent.click(unpublishButton); + } + + // Check if the modal for a new project is displayed + const projectNameInput = queryByTestId('project-name-input'); + + if (projectNameInput) { + // entering a project name in the modal + fireEvent.change(projectNameInput, { target: { value: 'My Project' } }); + } + }); + }); + + it('handles unpublish correctly', async () => { + const unpublishProjectMock = jest.spyOn(projectFunctions, 'unpublishProject'); + unpublishProjectMock.mockResolvedValueOnce({ + _id: 'mockedId', + name: 'Mocked Project', + published: false, + }); + + const store = configureStore({ + reducer: rootReducer, + preloadedState: { + appState: { + ...appStateInitialState, + isLoggedIn: true, + name: 'Mock Project Name', + HTMLTypes: mockHTMLTypes, + }, + }, + }); + + console.log('Before rendering NavBar'); + + const { queryByText } = renderNavBar(store); + + console.log('After rendering NavBar'); + + // Find the "Publish" or "Unpublish" button based on the project's publish state + const publishButton = queryByText('Publish'); + const unpublishButton = queryByText('Unpublish'); + + if (publishButton) { + fireEvent.click(publishButton); + } else if (unpublishButton) { + fireEvent.click(unpublishButton); + } + }); + + it('handles export correctly', async () => { + const store = configureStore({ + reducer: rootReducer, + preloadedState: { + appState: { + ...appStateInitialState, + isLoggedIn: true, + name: 'Mock Project Name', + HTMLTypes: mockHTMLTypes, + }, + }, + }); + + console.log('Before rendering NavBar'); + + const { getByText } = renderNavBar(store); + + console.log('After rendering NavBar'); + + // Find and click the export button + const exportButton = getByText('< > Export'); + fireEvent.click(exportButton); + + // Check if the modal for export options is displayed + await waitFor(() => { + const exportModal = getByText('Click to download in zip file:'); + expect(exportModal).toBeInTheDocument(); + }); + + // Simulate clicking the export components + const exportComponentsOption = getByText('Export components'); + fireEvent.click(exportComponentsOption); + + }); + + it('handles dropdown menu correctly', async () => { + const store = configureStore({ + reducer: rootReducer, + preloadedState: { + appState: { + ...appStateInitialState, + }, + }, + }); + + console.log('Before rendering NavBar'); + + const { getByTestId, getByText } = render( + + + + + + + + ); + + console.log('After rendering NavBar'); + + await act(async () => { + + const dropdownMenu = getByTestId('navDropDown'); + expect(dropdownMenu).toHaveClass('hideNavDropDown'); + + + const moreVertButton = getByTestId('more-vert-button'); + fireEvent.click(moreVertButton); + + + expect(dropdownMenu).toHaveClass('hideNavDropDown'); + + + const clearCanvasMenuItem = getByText('Clear Canvas'); + fireEvent.click(clearCanvasMenuItem); + expect(dropdownMenu).toHaveClass('hideNavDropDown'); + + + const marketplaceMenuItem = getByText('Marketplace'); + fireEvent.click(marketplaceMenuItem); + expect(dropdownMenu).toHaveClass('hideNavDropDown'); + + fireEvent.click(moreVertButton); + + expect(dropdownMenu).toHaveClass('hideNavDropDown'); + }); + }); +}) diff --git a/__tests__/componentReducer.test.ts b/__tests__/componentReducer.test.ts index 72f736449..142a45047 100644 --- a/__tests__/componentReducer.test.ts +++ b/__tests__/componentReducer.test.ts @@ -1,298 +1,298 @@ -import reducer from '../app/src/redux/reducers/slice/appStateSlice'; -import { State, Action } from '../app/src/interfaces/Interfaces'; -import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; - -describe('componentReducer Test', () => { - let state: State = initialState; - - // TEST 'ADD COMPONENT' - describe('addComponent reducer', () => { - it('should add new reuseable component to state', () => { - const action: Action = { - type: 'appState/addComponent', - payload: { - componentName: 'TestRegular', - root: false, - contextParam: { - allContext: [] - } - } - }; - state = reducer(state, action); - // expect state.components array to have length 2 - const length = state.components.length; - expect(length).toEqual(2); - // expect new component name to match name of last elem in state.components array - expect(state.components[length - 1].name).toEqual( - action.payload.componentName - ); - }); - }); - - // TEST 'ADD COMPONENT' with new root - describe('addComponent', () => { - it('should add new reuseable component to state as the new root', () => { - const action: Action = { - type: 'appState/addComponent', - payload: { - componentName: 'TestRootChange', - id: 3, - root: true, - contextParam: { - allContext: [] - } - } - }; - state = reducer(state, action); - - // expect state.components array to have length 3 - const length = state.components.length; - expect(length).toEqual(3); - // expect new root to match id of component id of TestRootChange (rootComponents is an array of component ID numbers) - expect(state.rootComponents[state.rootComponents.length - 1]).toEqual( - action.payload.id - ); - }); - }); - - // TEST 'ADD CHILD' - describe('addChild', () => { - it('should add child component and separator to top-level component', () => { - const action: Action = { - type: 'appState/addChild', - payload: { - type: 'Component', - typeId: 2, - childId: null, - contextParam: { - allContext: [] - } - } - }; - // switch focus to very first root component - // state.canvasFocus = { componentId: 1, childId: null }; - state = reducer(state, action); - const newParent = state.components[0]; - // expect new parent's children array to have length 2 (component + separator) - expect(newParent.children.length).toEqual(2); - // expect first element in children array to be separator - expect(newParent.children[0].name).toEqual('separator'); - // expect new child to have type 'Component' - expect(newParent.children[1].type).toEqual('Component'); - const addedChild = state.components.find( - (comp) => comp.id === newParent.children[1].typeId - ); - // expect new child typeId to correspond to component with name 'TestRegular' - expect(addedChild.name).toEqual('TestRegular'); - }); - }); - - // TEST 'CHANGE POSITION' - describe('changePosition', () => { - it('should move position of an instance', () => { - const actionHtml: Action = { - type: 'appState/addChild', - payload: { - type: 'HTML Element', - typeId: 9, - childId: null, - contextParam: { - allContext: [] - } - } - }; - state = reducer(state, actionHtml); - const actionChangePos: Action = { - type: 'appState/changePosition', - payload: { - currentChildId: 1, - newParentChildId: null, - contextParam: { - allContext: [] - } - } - }; - - state = reducer(state, actionChangePos); - const changeParent = state.components.find( - (comp) => comp.id === state.canvasFocus.componentId - ); - const changeParentChildLength = changeParent.children.length; - // expect last child of parent to be moved Component element - expect(changeParent.children[changeParentChildLength - 1].type).toEqual( - 'Component' - ); - // expect last child of parent to have current child ID of payload - expect( - changeParent.children[changeParentChildLength - 1].childId - ).toEqual(1); - }); - }); - - // TEST 'UPDATE CSS' - describe('updateCss', () => { - it('should add style to focused component', () => { - const action: Action = { - type: 'appState/updateCss', - payload: { - style: { - backgroundColor: 'gray' - }, - contextParam: { - allContext: [] - } - } - }; - state = reducer(state, action); - // expect the style property on targeted comp to equal style property in payload - expect(state.components[0].children[1].style).toEqual( - action.payload.style - ); - }); - }); - - // TEST 'DELETE CHILD' - describe('deleteChild', () => { - it('should delete child of focused top-level component', () => { - // canvas still focused on childId: 2, which is an HTML element - const action: Action = { - type: 'appState/deleteChild', - payload: { - id: 2, - contextParam: { - allContext: [] - } - } - }; - state = reducer(state, action); - // expect only one remaining child - const delParent = state.components.find( - (comp) => comp.id === state.canvasFocus.componentId - ); - // expect remaining child to have type 'Component' and to be preceded by separator - expect(delParent.children.length).toEqual(2); - expect(delParent.children[delParent.children.length - 1].type).toEqual( - 'Component' - ); - expect(delParent.children[delParent.children.length - 2].name).toEqual( - 'separator' - ); - }); - }); - - // TEST 'CHANGE FOCUS' - describe('changeFocus', () => { - it('should change focus to specified component', () => { - const action: Action = { - type: 'appState/changeFocus', - payload: { - componentId: 2, - childId: null - } - }; - state = reducer(state, action); - expect(state.canvasFocus.componentId).toEqual(2); - expect(state.canvasFocus.childId).toEqual(null); - }); - }); - - // TEST 'UPDATE PROJECT NAME' - describe('updateProjectName', () => { - it('should update project with specified name', () => { - const action: Action = { - type: 'appState/updateProjectName', - payload: 'TESTNAME' - }; - state = reducer(state, action); - // expect state name to equal payload - expect(state.name).toEqual(action.payload); - }); - }); - - // TEST 'CHANGE PROJECT TYPE' - describe('changeProjectType', () => { - it('should change project type to specified type', () => { - const action: Action = { - type: 'appState/changeProjectType', - payload: { - projectType: 'Classic React', - contextParam: { - allContext: [] - } - } - }; - state = reducer(state, action); - expect(state.projectType).toEqual(action.payload.projectType); - }); - }); - - // TEST 'UNDO' - xdescribe('undo', () => { - it('should remove the last element from the past array and push it to the future array', () => { - const focusIndex = state.canvasFocus.componentId - 1; - state.components[focusIndex].past = []; - - // snapShotFunc taken from src/components/main/canvas.tsx to test undo functionality - // snapShotFunc takes a snapshot of state to be used in UNDO/REDO functionality - const snapShotFuncCopy = () => { - const deepCopiedState = JSON.parse(JSON.stringify(state)); - // pushes the last user action on the canvas into the past array of Component - state.components[focusIndex].past.push( - deepCopiedState.components[focusIndex].children - ); - }; - - const actionHTML2: Action = { - type: 'appState/addChild', - payload: { - type: 'HTML Element', - typeId: 4, - childId: null - } - }; - state = reducer(state, actionHTML2); - // invoking snapShotFunc is necessary to push actions into the past array, referenced in the UNDO functionality to define children - snapShotFuncCopy(); - - const actionUndo: Action = { - type: 'appState/undo', - payload: {} - }; - state = reducer(state, actionUndo); - - expect(state.components[focusIndex].past.length).toEqual(0); - expect(state.components[focusIndex].future.length).toEqual(1); - }); - }); - - // TEST 'REDO' - xdescribe('redo', () => { - it('should remove the last element from the future array and push it to the past array', () => { - const focusIndex = state.canvasFocus.componentId - 1; - const actionRedo: Action = { - type: 'appState/redo', - payload: {} - }; - state = reducer(state, actionRedo); - expect(state.components[focusIndex].future.length).toEqual(0); - expect(state.components[focusIndex].past.length).toEqual(1); - }); - }); - - // TEST 'RESET STATE' - describe('resetState', () => { - it('should reset project to initial state', () => { - const action: Action = { - type: 'appState/resetState', - payload: '' - }; - state = reducer(state, action); - // expect default project to have empty string as name - expect(state.name).toEqual('TESTNAME'); - // expect default project to only have one component in components array - expect(state.components.length).toEqual(1); - // expect lone component to have no children :( - expect(state.components[0].children.length).toEqual(0); - }); - }); -}); +import reducer from '../app/src/redux/reducers/slice/appStateSlice'; +import { State, Action } from '../app/src/interfaces/Interfaces'; +import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; + +describe('componentReducer Test', () => { + let state: State = initialState; + + // TEST 'ADD COMPONENT' + describe('addComponent reducer', () => { + it('should add new reuseable component to state', () => { + const action: Action = { + type: 'appState/addComponent', + payload: { + componentName: 'TestRegular', + root: false, + contextParam: { + allContext: [] + } + } + }; + state = reducer(state, action); + // expect state.components array to have length 2 + const length = state.components.length; + expect(length).toEqual(2); + // expect new component name to match name of last elem in state.components array + expect(state.components[length - 1].name).toEqual( + action.payload.componentName + ); + }); + }); + + // TEST 'ADD COMPONENT' with new root + describe('addComponent', () => { + it('should add new reuseable component to state as the new root', () => { + const action: Action = { + type: 'appState/addComponent', + payload: { + componentName: 'TestRootChange', + id: 3, + root: true, + contextParam: { + allContext: [] + } + } + }; + state = reducer(state, action); + + // expect state.components array to have length 3 + const length = state.components.length; + expect(length).toEqual(3); + // expect new root to match id of component id of TestRootChange (rootComponents is an array of component ID numbers) + expect(state.rootComponents[state.rootComponents.length - 1]).toEqual( + action.payload.id + ); + }); + }); + + // TEST 'ADD CHILD' + describe('addChild', () => { + it('should add child component and separator to top-level component', () => { + const action: Action = { + type: 'appState/addChild', + payload: { + type: 'Component', + typeId: 2, + childId: null, + contextParam: { + allContext: [] + } + } + }; + // switch focus to very first root component + // state.canvasFocus = { componentId: 1, childId: null }; + state = reducer(state, action); + const newParent = state.components[0]; + // expect new parent's children array to have length 2 (component + separator) + expect(newParent.children.length).toEqual(2); + // expect first element in children array to be separator + expect(newParent.children[0].name).toEqual('separator'); + // expect new child to have type 'Component' + expect(newParent.children[1].type).toEqual('Component'); + const addedChild = state.components.find( + (comp) => comp.id === newParent.children[1].typeId + ); + // expect new child typeId to correspond to component with name 'TestRegular' + expect(addedChild.name).toEqual('TestRegular'); + }); + }); + + // TEST 'CHANGE POSITION' + describe('changePosition', () => { + it('should move position of an instance', () => { + const actionHtml: Action = { + type: 'appState/addChild', + payload: { + type: 'HTML Element', + typeId: 9, + childId: null, + contextParam: { + allContext: [] + } + } + }; + state = reducer(state, actionHtml); + const actionChangePos: Action = { + type: 'appState/changePosition', + payload: { + currentChildId: 1, + newParentChildId: null, + contextParam: { + allContext: [] + } + } + }; + + state = reducer(state, actionChangePos); + const changeParent = state.components.find( + (comp) => comp.id === state.canvasFocus.componentId + ); + const changeParentChildLength = changeParent.children.length; + // expect last child of parent to be moved Component element + expect(changeParent.children[changeParentChildLength - 1].type).toEqual( + 'Component' + ); + // expect last child of parent to have current child ID of payload + expect( + changeParent.children[changeParentChildLength - 1].childId + ).toEqual(1); + }); + }); + + // TEST 'UPDATE CSS' + describe('updateCss', () => { + it('should add style to focused component', () => { + const action: Action = { + type: 'appState/updateCss', + payload: { + style: { + backgroundColor: 'gray' + }, + contextParam: { + allContext: [] + } + } + }; + state = reducer(state, action); + // expect the style property on targeted comp to equal style property in payload + expect(state.components[0].children[1].style).toEqual( + action.payload.style + ); + }); + }); + + // TEST 'DELETE CHILD' + describe('deleteChild', () => { + it('should delete child of focused top-level component', () => { + // canvas still focused on childId: 2, which is an HTML element + const action: Action = { + type: 'appState/deleteChild', + payload: { + id: 2, + contextParam: { + allContext: [] + } + } + }; + state = reducer(state, action); + // expect only one remaining child + const delParent = state.components.find( + (comp) => comp.id === state.canvasFocus.componentId + ); + // expect remaining child to have type 'Component' and to be preceded by separator + expect(delParent.children.length).toEqual(2); + expect(delParent.children[delParent.children.length - 1].type).toEqual( + 'Component' + ); + expect(delParent.children[delParent.children.length - 2].name).toEqual( + 'separator' + ); + }); + }); + + // TEST 'CHANGE FOCUS' + describe('changeFocus', () => { + it('should change focus to specified component', () => { + const action: Action = { + type: 'appState/changeFocus', + payload: { + componentId: 2, + childId: null + } + }; + state = reducer(state, action); + expect(state.canvasFocus.componentId).toEqual(2); + expect(state.canvasFocus.childId).toEqual(null); + }); + }); + + // TEST 'UPDATE PROJECT NAME' + describe('updateProjectName', () => { + it('should update project with specified name', () => { + const action: Action = { + type: 'appState/updateProjectName', + payload: 'TESTNAME' + }; + state = reducer(state, action); + // expect state name to equal payload + expect(state.name).toEqual(action.payload); + }); + }); + + // TEST 'CHANGE PROJECT TYPE' + describe('changeProjectType', () => { + it('should change project type to specified type', () => { + const action: Action = { + type: 'appState/changeProjectType', + payload: { + projectType: 'Classic React', + contextParam: { + allContext: [] + } + } + }; + state = reducer(state, action); + expect(state.projectType).toEqual(action.payload.projectType); + }); + }); + + // TEST 'UNDO' + xdescribe('undo', () => { + it('should remove the last element from the past array and push it to the future array', () => { + const focusIndex = state.canvasFocus.componentId - 1; + state.components[focusIndex].past = []; + + // snapShotFunc taken from src/components/main/canvas.tsx to test undo functionality + // snapShotFunc takes a snapshot of state to be used in UNDO/REDO functionality + const snapShotFuncCopy = () => { + const deepCopiedState = JSON.parse(JSON.stringify(state)); + // pushes the last user action on the canvas into the past array of Component + state.components[focusIndex].past.push( + deepCopiedState.components[focusIndex].children + ); + }; + + const actionHTML2: Action = { + type: 'appState/addChild', + payload: { + type: 'HTML Element', + typeId: 4, + childId: null + } + }; + state = reducer(state, actionHTML2); + // invoking snapShotFunc is necessary to push actions into the past array, referenced in the UNDO functionality to define children + snapShotFuncCopy(); + + const actionUndo: Action = { + type: 'appState/undo', + payload: {} + }; + state = reducer(state, actionUndo); + + expect(state.components[focusIndex].past.length).toEqual(0); + expect(state.components[focusIndex].future.length).toEqual(1); + }); + }); + + // TEST 'REDO' + xdescribe('redo', () => { + it('should remove the last element from the future array and push it to the past array', () => { + const focusIndex = state.canvasFocus.componentId - 1; + const actionRedo: Action = { + type: 'appState/redo', + payload: {} + }; + state = reducer(state, actionRedo); + expect(state.components[focusIndex].future.length).toEqual(0); + expect(state.components[focusIndex].past.length).toEqual(1); + }); + }); + + // TEST 'RESET STATE' + describe('resetState', () => { + it('should reset project to initial state', () => { + const action: Action = { + type: 'appState/resetState', + payload: '' + }; + state = reducer(state, action); + // expect default project to have empty string as name + expect(state.name).toEqual('TESTNAME'); + // expect default project to only have one component in components array + expect(state.components.length).toEqual(1); + // expect lone component to have no children :( + expect(state.components[0].children.length).toEqual(0); + }); + }); +}); diff --git a/__tests__/contextReducer.test.js b/__tests__/contextReducer.test.js index 72b98944f..31b9db262 100644 --- a/__tests__/contextReducer.test.js +++ b/__tests__/contextReducer.test.js @@ -1,195 +1,195 @@ -import subject from '../app/src/redux/reducers/slice/contextReducer.ts'; - -describe('contextReducer test', () => { - let state; - - beforeEach(() => { - state = { - allContext: [], - }; - }); - - describe('default state', () => { - it('should return a default state when given an undefined input', () => { - expect(subject(undefined, { type: undefined })).toEqual(state); - }); - }); - - describe('unrecognized action types', () => { - it('should return the original state without any duplication', () => { - expect(subject(state, { type: 'REMOVE_STATE' })).toBe(state); - }); - }); - - describe('addContext', () => { - const action = { - type: 'context/addContext', - payload: { - name: 'Theme Context', - }, - }; - - it('adds a context', () => { - const { allContext } = subject(state, action); - expect(allContext[0]).toEqual({ - name: 'Theme Context', - values: [], - components: [], - }); - }); - - it('returns a state object not strictly equal to the original', () => { - const newState = subject(state, action); - expect(newState).not.toBe(state); - }); - - it('should immutably update the nested state object', () => { - const { allContext } = subject(state, action); - expect(allContext).not.toBe(state.allContext); - }); - }); - - // OLD ADD CONTEX TEST - - // describe('ADD_CONTEXT', () => { - // const action = { - // type: 'ADD_CONTEXT', - // payload: { - // name: 'Theme Context' - // } - // }; - - // it('adds a context', () => { - // const { allContext } = subject(state, action); - // expect(allContext[0]).toEqual({ - // name: 'Theme Context', - // values: [], - // components: [] - // }); - // }); - - // it('returns a state object not strictly equal to the original', () => { - // const newState = subject(state, action); - // expect(newState).not.toBe(state); - // }); - - // it('should immutably update the nested state object', () => { - // const { allContext } = subject(state, action); - // expect(allContext).not.toBe(state.allContext); - // }); - // }); - - describe('addContextValues', () => { - beforeEach(() => { - state = { - allContext: [ - { - name: 'Theme Context', - values: [], - components: [], - }, - ], - }; - }); - - const action = { - type: 'context/addContextValues', - payload: { - name: 'Theme Context', - inputKey: 'Theme Color', - inputValue: 'Dark', - }, - }; - - it('adds a key-value pair to values array of the specified context', () => { - const { allContext } = subject(state, action); - expect(allContext[0].values.length).toEqual(1); - expect(allContext[0].values[0].key).toEqual('Theme Color'); - expect(allContext[0].values[0].value).toEqual('Dark'); - }); - - it('includes an allContext not strictly equal to the original', () => { - const { allContext } = subject(state, action); - - expect(allContext).not.toBe(state.allContext); - }); - }); - - describe('deleteContext', () => { - let action; - beforeEach(() => { - state = { - allContext: [ - { - name: 'Theme Context', - values: [], - components: [], - }, - { - name: 'To be deleted', - values: [], - components: [], - }, - ], - }; - - action = { - type: 'context/deleteContext', - payload: { - name: 'Theme Context', - }, - }; - }); - - it('removes specified context from the state', () => { - const { allContext } = subject(state, action); - - expect(allContext.length).toEqual(1); - }); - - it('includes an allContext not strictly equal to the original', () => { - const { allContext } = subject(state, action); - - expect(allContext).not.toBe(state.allContext); - }); - }); - - describe('addComponentToContext', () => { - beforeEach(() => { - state = { - allContext: [ - { - name: 'Theme Context', - values: [], - components: [], - }, - ], - }; - }); - - const action = { - type: 'context/addComponentToContext', - payload: { - context: { - name: 'Theme Context', - }, - component: { - name: 'Main Component', - }, - }, - }; - - it('adds a new component to the specified context', () => { - const { allContext } = subject(state, action); - - expect(allContext[0].components.length).toEqual(1); - expect(allContext[0].components[0]).toEqual('Main Component'); - }); - - it('includes an allContext not strictly equal to the original', () => { - const { allContext } = subject(state, action); - - expect(allContext).not.toBe(state.allContext); - }); - }); -}); +import subject from '../app/src/redux/reducers/slice/contextReducer.ts'; + +describe('contextReducer test', () => { + let state; + + beforeEach(() => { + state = { + allContext: [], + }; + }); + + describe('default state', () => { + it('should return a default state when given an undefined input', () => { + expect(subject(undefined, { type: undefined })).toEqual(state); + }); + }); + + describe('unrecognized action types', () => { + it('should return the original state without any duplication', () => { + expect(subject(state, { type: 'REMOVE_STATE' })).toBe(state); + }); + }); + + describe('addContext', () => { + const action = { + type: 'context/addContext', + payload: { + name: 'Theme Context', + }, + }; + + it('adds a context', () => { + const { allContext } = subject(state, action); + expect(allContext[0]).toEqual({ + name: 'Theme Context', + values: [], + components: [], + }); + }); + + it('returns a state object not strictly equal to the original', () => { + const newState = subject(state, action); + expect(newState).not.toBe(state); + }); + + it('should immutably update the nested state object', () => { + const { allContext } = subject(state, action); + expect(allContext).not.toBe(state.allContext); + }); + }); + + // OLD ADD CONTEX TEST + + // describe('ADD_CONTEXT', () => { + // const action = { + // type: 'ADD_CONTEXT', + // payload: { + // name: 'Theme Context' + // } + // }; + + // it('adds a context', () => { + // const { allContext } = subject(state, action); + // expect(allContext[0]).toEqual({ + // name: 'Theme Context', + // values: [], + // components: [] + // }); + // }); + + // it('returns a state object not strictly equal to the original', () => { + // const newState = subject(state, action); + // expect(newState).not.toBe(state); + // }); + + // it('should immutably update the nested state object', () => { + // const { allContext } = subject(state, action); + // expect(allContext).not.toBe(state.allContext); + // }); + // }); + + describe('addContextValues', () => { + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [], + }, + ], + }; + }); + + const action = { + type: 'context/addContextValues', + payload: { + name: 'Theme Context', + inputKey: 'Theme Color', + inputValue: 'Dark', + }, + }; + + it('adds a key-value pair to values array of the specified context', () => { + const { allContext } = subject(state, action); + expect(allContext[0].values.length).toEqual(1); + expect(allContext[0].values[0].key).toEqual('Theme Color'); + expect(allContext[0].values[0].value).toEqual('Dark'); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('deleteContext', () => { + let action; + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [], + }, + { + name: 'To be deleted', + values: [], + components: [], + }, + ], + }; + + action = { + type: 'context/deleteContext', + payload: { + name: 'Theme Context', + }, + }; + }); + + it('removes specified context from the state', () => { + const { allContext } = subject(state, action); + + expect(allContext.length).toEqual(1); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); + + describe('addComponentToContext', () => { + beforeEach(() => { + state = { + allContext: [ + { + name: 'Theme Context', + values: [], + components: [], + }, + ], + }; + }); + + const action = { + type: 'context/addComponentToContext', + payload: { + context: { + name: 'Theme Context', + }, + component: { + name: 'Main Component', + }, + }, + }; + + it('adds a new component to the specified context', () => { + const { allContext } = subject(state, action); + + expect(allContext[0].components.length).toEqual(1); + expect(allContext[0].components[0]).toEqual('Main Component'); + }); + + it('includes an allContext not strictly equal to the original', () => { + const { allContext } = subject(state, action); + + expect(allContext).not.toBe(state.allContext); + }); + }); +}); diff --git a/__tests__/gql.projects.test.ts b/__tests__/gql.projects.test.ts index 212ea7c7f..e77fa2a22 100644 --- a/__tests__/gql.projects.test.ts +++ b/__tests__/gql.projects.test.ts @@ -1,143 +1,143 @@ -// /** -// * @jest-environment node -// */ - -// const { Mongoose } = require('mongoose'); -// const request = require('supertest'); -// const http = require('http'); -// const app = require('../server/server'); -// const mock = require('../mockData'); - -// tests user signup and login routes -xdescribe('GraphQL tests', () => { - let server; - // Mutation test variables - const projectId = '62fd62c6d37748133a6fdc81'; // Must use a valid projectId from the database. NOTE: This should be revised for each Production Project Team since the database store different projectId - const testNum = 100; - const makeCopyUserIdTest = '604333d10004ad51c899e250'; - const makeCopyUsernameTest = 'test1'; - let makeCopyProjId = ''; - beforeAll((done) => { - server = http.createServer(app); - server.listen(done); - }); - afterAll((done) => { - Mongoose.disconnect(); - server.close(done); - }); - // GraphQL Query - - xdescribe('Testing GraphQL query', () => { - it('getAllProjects should return more than 1 project by default', () => - request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.GET_PROJECTS - }) - .expect(200) - .then((res) => - expect(res.body.data.getAllProjects.length).toBeGreaterThanOrEqual(1) - )); - it('getAllProjects should return projects that matches the provided userId', () => - request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.GET_PROJECTS, - variables: { - userId: '604d21b2b61a1c95f2dc9105' - } - }) - .expect(200) - .then((res) => - expect(res.body.data.getAllProjects[0].userId).toBe( - '604d21b2b61a1c95f2dc9105' - ) - )); - }); - // GraphQL Mutation - - xdescribe('Testing GraphQL mutation', () => { - // Add likes - it('addLike should update the "likes" field of the project document', () => - request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.ADD_LIKE, - variables: { - projId: projectId, - likes: testNum - } - }) - .expect(200) - .then((res) => expect(res.body.data.addLike.likes).toBe(testNum))); - // Publish project - it('Should set the "published" on the project document to TRUE', () => - request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.PUBLISH_PROJECT, - variables: { - projId: projectId, - published: true - } - }) - .expect(200) - .then((res) => - expect(res.body.data.publishProject.published).toBe(true) - )); - it('Should set the "published" on the project document to FALSE', () => - request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.PUBLISH_PROJECT, - variables: { - projId: projectId, - published: false - } - }) - .expect(200) - .then((res) => - expect(res.body.data.publishProject.published).toBe(false) - )); - // Make copy - it('Should make a copy of an existing project and change the userId and userName', () => - request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.MAKE_COPY, - variables: { - projId: projectId, - userId: makeCopyUserIdTest, - username: makeCopyUsernameTest - } - }) - .expect(200) - .then((res) => { - expect(res.body.data.makeCopy.userId).toBe(makeCopyUserIdTest); - expect(res.body.data.makeCopy.username).toBe(makeCopyUsernameTest); - makeCopyProjId = res.body.data.makeCopy.id; - })); - - // Delete copy - it('Should make a copy of an existing project and change the userId and userName', () => - request(server) - .post('/graphql') - .set('Content-Type', 'application/json') - .send({ - query: mock.DELETE_PROJECT, - variables: { - projId: makeCopyProjId - } - }) - .expect(200) - .then((res) => { - expect(res.body.data.deleteProject.id).toBe(makeCopyProjId); - })); - }); -}); +// /** +// * @jest-environment node +// */ + +// const { Mongoose } = require('mongoose'); +// const request = require('supertest'); +// const http = require('http'); +// const app = require('../server/server'); +// const mock = require('../mockData'); + +// tests user signup and login routes +xdescribe('GraphQL tests', () => { + let server; + // Mutation test variables + const projectId = '62fd62c6d37748133a6fdc81'; // Must use a valid projectId from the database. NOTE: This should be revised for each Production Project Team since the database store different projectId + const testNum = 100; + const makeCopyUserIdTest = '604333d10004ad51c899e250'; + const makeCopyUsernameTest = 'test1'; + let makeCopyProjId = ''; + beforeAll((done) => { + server = http.createServer(app); + server.listen(done); + }); + afterAll((done) => { + Mongoose.disconnect(); + server.close(done); + }); + // GraphQL Query + + xdescribe('Testing GraphQL query', () => { + it('getAllProjects should return more than 1 project by default', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.GET_PROJECTS + }) + .expect(200) + .then((res) => + expect(res.body.data.getAllProjects.length).toBeGreaterThanOrEqual(1) + )); + it('getAllProjects should return projects that matches the provided userId', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.GET_PROJECTS, + variables: { + userId: '604d21b2b61a1c95f2dc9105' + } + }) + .expect(200) + .then((res) => + expect(res.body.data.getAllProjects[0].userId).toBe( + '604d21b2b61a1c95f2dc9105' + ) + )); + }); + // GraphQL Mutation + + xdescribe('Testing GraphQL mutation', () => { + // Add likes + it('addLike should update the "likes" field of the project document', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.ADD_LIKE, + variables: { + projId: projectId, + likes: testNum + } + }) + .expect(200) + .then((res) => expect(res.body.data.addLike.likes).toBe(testNum))); + // Publish project + it('Should set the "published" on the project document to TRUE', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.PUBLISH_PROJECT, + variables: { + projId: projectId, + published: true + } + }) + .expect(200) + .then((res) => + expect(res.body.data.publishProject.published).toBe(true) + )); + it('Should set the "published" on the project document to FALSE', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.PUBLISH_PROJECT, + variables: { + projId: projectId, + published: false + } + }) + .expect(200) + .then((res) => + expect(res.body.data.publishProject.published).toBe(false) + )); + // Make copy + it('Should make a copy of an existing project and change the userId and userName', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.MAKE_COPY, + variables: { + projId: projectId, + userId: makeCopyUserIdTest, + username: makeCopyUsernameTest + } + }) + .expect(200) + .then((res) => { + expect(res.body.data.makeCopy.userId).toBe(makeCopyUserIdTest); + expect(res.body.data.makeCopy.username).toBe(makeCopyUsernameTest); + makeCopyProjId = res.body.data.makeCopy.id; + })); + + // Delete copy + it('Should make a copy of an existing project and change the userId and userName', () => + request(server) + .post('/graphql') + .set('Content-Type', 'application/json') + .send({ + query: mock.DELETE_PROJECT, + variables: { + projId: makeCopyProjId + } + }) + .expect(200) + .then((res) => { + expect(res.body.data.deleteProject.id).toBe(makeCopyProjId); + })); + }); +}); diff --git a/__tests__/helper.test.tsx b/__tests__/helper.test.tsx index 14ce14b3b..c5f62655e 100644 --- a/__tests__/helper.test.tsx +++ b/__tests__/helper.test.tsx @@ -1,7 +1,7 @@ -import randomPassword from '../app/src/helperFunctions/randomPassword'; - -describe('Random Password', () => { - test('should generate password with 18 characters', () => { - expect(randomPassword()).toHaveLength(18); - }); -}); +import randomPassword from '../app/src/helperFunctions/randomPassword'; + +describe('Random Password', () => { + test('should generate password with 18 characters', () => { + expect(randomPassword()).toHaveLength(18); + }); +}); diff --git a/__tests__/marketplace.test.tsx b/__tests__/marketplace.test.tsx index d08680931..f1c8a38c6 100644 --- a/__tests__/marketplace.test.tsx +++ b/__tests__/marketplace.test.tsx @@ -1,133 +1,133 @@ -import '@testing-library/jest-dom/extend-expect'; - -import { fireEvent, render, screen } from '@testing-library/react'; - -import MarketplaceCard from '../app/src/components/marketplace/MarketplaceCard'; -import MarketplaceCardContainer from '../app/src/components/marketplace/MarketplaceCardContainer'; -import { Provider } from 'react-redux'; -import React from 'react'; -import SearchBar from '../app/src/components/marketplace/Searchbar'; -import axios from 'axios'; -import store from '../app/src/redux/store'; - -// Mocking the axios module to avoid actual network calls -jest.mock('axios'); -jest.mock( - 'resources/marketplace_images/marketplace_image.png', - () => 'mock-image-url' -); - -describe('MarketplaceCard Render Test', () => { - const mockProject = { - _id: 123, - name: 'Sample Project', - username: 'user123', - forked: 'false', - comments: [], - createdAt: new Date(), - likes: 0, - project: { - id: 'sample-project-id' - }, - published: true, - userId: 123456 - }; - - it('displays project name and username', () => { - render( - - - - ); - - expect(screen.getByText('Sample Project')).toBeInTheDocument(); - expect(screen.getByText('user123')).toBeInTheDocument(); - }); -}); - -describe('MarketplaceContainer', () => { - const mockProjects = [ - { - _id: 1, - name: 'Project 1', - username: 'user1' - }, - { - _id: 2, - name: 'Project 2', - username: 'user2' - } - ]; - - beforeEach(() => { - // Set up mock axios call for every test - axios.get = jest.fn().mockResolvedValue({ data: mockProjects }); - }); - - it('renders multiple MarketplaceCards', () => { - render( - - - - ); - - expect(screen.getByText('Project 1')).toBeInTheDocument(); - expect(screen.getByText('user1')).toBeInTheDocument(); - expect(screen.getByText('Project 2')).toBeInTheDocument(); - expect(screen.getByText('user2')).toBeInTheDocument(); - }); -}); - -const mockProjects = [ - { - name: 'Sample Project', - username: 'user123' - }, - { - name: 'Test Project', - username: 'user_test' - }, - { - name: 'Hello Project', - username: 'hello_user' - } -]; - -describe('SearchBar Component', () => { - it('updates the text field value on change', () => { - const updateDisplayProjects = jest.fn(); - - render( - - ); - - const textField = screen.getByLabelText('Search') as HTMLInputElement; - fireEvent.change(textField, { target: { value: 'Sample' } }); - - expect(textField.value).toBe('Sample'); - }); - - it('filters projects by username', () => { - const updateDisplayProjects = jest.fn(); - - render( - - ); - - const textField = screen.getByLabelText('Search'); - fireEvent.change(textField, { target: { value: 'test' } }); - - // Using setImmediate to wait for useEffect to execute. - setTimeout(() => { - expect(updateDisplayProjects).toHaveBeenCalledWith([ - { name: 'Test Project', username: 'user_test' } - ]); - }); - }); -}); +import '@testing-library/jest-dom/extend-expect'; + +import { fireEvent, render, screen } from '@testing-library/react'; + +import MarketplaceCard from '../app/src/components/marketplace/MarketplaceCard'; +import MarketplaceCardContainer from '../app/src/components/marketplace/MarketplaceCardContainer'; +import { Provider } from 'react-redux'; +import React from 'react'; +import SearchBar from '../app/src/components/marketplace/Searchbar'; +import axios from 'axios'; +import store from '../app/src/redux/store'; + +// Mocking the axios module to avoid actual network calls +jest.mock('axios'); +jest.mock( + 'resources/marketplace_images/marketplace_image.png', + () => 'mock-image-url' +); + +describe('MarketplaceCard Render Test', () => { + const mockProject = { + _id: 123, + name: 'Sample Project', + username: 'user123', + forked: 'false', + comments: [], + createdAt: new Date(), + likes: 0, + project: { + id: 'sample-project-id' + }, + published: true, + userId: 123456 + }; + + it('displays project name and username', () => { + render( + + + + ); + + expect(screen.getByText('Sample Project')).toBeInTheDocument(); + expect(screen.getByText('user123')).toBeInTheDocument(); + }); +}); + +describe('MarketplaceContainer', () => { + const mockProjects = [ + { + _id: 1, + name: 'Project 1', + username: 'user1' + }, + { + _id: 2, + name: 'Project 2', + username: 'user2' + } + ]; + + beforeEach(() => { + // Set up mock axios call for every test + axios.get = jest.fn().mockResolvedValue({ data: mockProjects }); + }); + + it('renders multiple MarketplaceCards', () => { + render( + + + + ); + + expect(screen.getByText('Project 1')).toBeInTheDocument(); + expect(screen.getByText('user1')).toBeInTheDocument(); + expect(screen.getByText('Project 2')).toBeInTheDocument(); + expect(screen.getByText('user2')).toBeInTheDocument(); + }); +}); + +const mockProjects = [ + { + name: 'Sample Project', + username: 'user123' + }, + { + name: 'Test Project', + username: 'user_test' + }, + { + name: 'Hello Project', + username: 'hello_user' + } +]; + +describe('SearchBar Component', () => { + it('updates the text field value on change', () => { + const updateDisplayProjects = jest.fn(); + + render( + + ); + + const textField = screen.getByLabelText('Search') as HTMLInputElement; + fireEvent.change(textField, { target: { value: 'Sample' } }); + + expect(textField.value).toBe('Sample'); + }); + + it('filters projects by username', () => { + const updateDisplayProjects = jest.fn(); + + render( + + ); + + const textField = screen.getByLabelText('Search'); + fireEvent.change(textField, { target: { value: 'test' } }); + + // Using setImmediate to wait for useEffect to execute. + setTimeout(() => { + expect(updateDisplayProjects).toHaveBeenCalledWith([ + { name: 'Test Project', username: 'user_test' } + ]); + }); + }); +}); diff --git a/__tests__/playwright/example.spec.ts b/__tests__/playwright/example.spec.ts index bf3d92ab5..67ffac6f9 100644 --- a/__tests__/playwright/example.spec.ts +++ b/__tests__/playwright/example.spec.ts @@ -1,18 +1,18 @@ -// import { test, expect } from '@playwright/test'; - -// test('has title', async ({ page }) => { -// await page.goto('https://app.reactype.dev/#/'); - -// // Expect a title "to contain" a substring. -// await expect(page).toHaveTitle('ReacType'); -// }); - -// test('get started link', async ({ page }) => { -// await page.goto('https://playwright.dev/'); - -// // Click the get started link. -// await page.getByRole('link', { name: 'Get started' }).click(); - -// // Expects the URL to contain intro. -// await expect(page).toHaveURL(/.*intro/); -// }); +// import { test, expect } from '@playwright/test'; + +// test('has title', async ({ page }) => { +// await page.goto('https://app.reactype.dev/#/'); + +// // Expect a title "to contain" a substring. +// await expect(page).toHaveTitle('ReacType'); +// }); + +// test('get started link', async ({ page }) => { +// await page.goto('https://playwright.dev/'); + +// // Click the get started link. +// await page.getByRole('link', { name: 'Get started' }).click(); + +// // Expects the URL to contain intro. +// await expect(page).toHaveURL(/.*intro/); +// }); diff --git a/__tests__/projects.test.ts b/__tests__/projects.test.ts index f9ec600e1..4b65abfb2 100644 --- a/__tests__/projects.test.ts +++ b/__tests__/projects.test.ts @@ -1,71 +1,71 @@ -/** - * @jest-environment node - */ - -const { Mongoose } = require('mongoose'); -const request = require('supertest'); -// initializes the project to be sent to server/DB -import mockData from '../mockData'; -import app from '../server/server'; -const http = require('http'); -const { state, projectToSave } = mockData; - -// save and get projects endpoint testing -describe('Project endpoints tests', () => { - let server; - beforeAll((done) => { - server = http.createServer(app); - server.listen(done); - }); - afterAll((done) => { - Mongoose.disconnect().then(() => { - // Close the HTTP server - server.close(done); - }); - }); - // test saveProject endpoint - describe('/saveProject', () => { - describe('/POST', () => { - it('responds with a status of 200 and json object equal to project sent', () => { - return request(server) - .post('/saveProject') - .set('Accept', 'application/json') - .send(projectToSave) - .expect(200) - .expect('Content-Type', /application\/json/) - .then((res) => expect(res.body.name).toBe(projectToSave.name)); - }); - }); - }); - // test getProjects endpoint - describe('/getProjects', () => { - describe('POST', () => { - it('responds with status of 200 and json object equal to an array of user projects', () => { - return request(server) - .post('/getProjects') - .set('Accept', 'application/json') - .send({ userId: projectToSave.userId }) - .expect(200) - .expect('Content-Type', /json/) - .then((res) => { - expect(Array.isArray(res.body)).toBeTruthy; - expect(res.body[0].name).toBe(state.name); - }); - }); - }); - }); - // test deleteProject endpoint - describe('/deleteProject', () => { - describe('DELETE', () => { - const { name, userId } = projectToSave; - it('responds with status of 200 and json object equal to deleted project', () => { - return request(server) - .delete('/deleteProject') - .set('Accept', 'application/json') - .send({ name, userId }) - .expect(200) - .then((res) => expect(res.body.name).toBe(projectToSave.name)); - }); - }); - }); -}); +/** + * @jest-environment node + */ + +const { Mongoose } = require('mongoose'); +const request = require('supertest'); +// initializes the project to be sent to server/DB +import mockData from '../mockData'; +import app from '../server/server'; +const http = require('http'); +const { state, projectToSave } = mockData; + +// save and get projects endpoint testing +describe('Project endpoints tests', () => { + let server; + beforeAll((done) => { + server = http.createServer(app); + server.listen(done); + }); + afterAll((done) => { + Mongoose.disconnect().then(() => { + // Close the HTTP server + server.close(done); + }); + }); + // test saveProject endpoint + describe('/saveProject', () => { + describe('/POST', () => { + it('responds with a status of 200 and json object equal to project sent', () => { + return request(server) + .post('/saveProject') + .set('Accept', 'application/json') + .send(projectToSave) + .expect(200) + .expect('Content-Type', /application\/json/) + .then((res) => expect(res.body.name).toBe(projectToSave.name)); + }); + }); + }); + // test getProjects endpoint + describe('/getProjects', () => { + describe('POST', () => { + it('responds with status of 200 and json object equal to an array of user projects', () => { + return request(server) + .post('/getProjects') + .set('Accept', 'application/json') + .send({ userId: projectToSave.userId }) + .expect(200) + .expect('Content-Type', /json/) + .then((res) => { + expect(Array.isArray(res.body)).toBeTruthy; + expect(res.body[0].name).toBe(state.name); + }); + }); + }); + }); + // test deleteProject endpoint + describe('/deleteProject', () => { + describe('DELETE', () => { + const { name, userId } = projectToSave; + it('responds with status of 200 and json object equal to deleted project', () => { + return request(server) + .delete('/deleteProject') + .set('Accept', 'application/json') + .send({ name, userId }) + .expect(200) + .then((res) => expect(res.body.name).toBe(projectToSave.name)); + }); + }); + }); +}); diff --git a/__tests__/server.test.tsx b/__tests__/server.test.tsx index d1c339383..62743c3d3 100644 --- a/__tests__/server.test.tsx +++ b/__tests__/server.test.tsx @@ -1,445 +1,445 @@ -/** - * @jest-environment node - */ - -import marketplaceController from '../server/controllers/marketplaceController'; -import sessionController from '../server/controllers/sessionController'; -import app from '../server/server'; -import mockData from '../mockData'; -import { profileEnd } from 'console'; -import { Projects, Users, Sessions } from '../server/models/reactypeModels'; -const request = require('supertest'); -const mongoose = require('mongoose'); -const mockNext = jest.fn(); // Mock nextFunction -const MONGO_DB = import.meta.env.MONGO_DB_TEST; -const { state, projectToSave, user } = mockData; -const PORT = 8080; - -beforeAll(async () => { - await mongoose.connect(MONGO_DB, { - useNewUrlParser: true, - useUnifiedTopology: true - }); -}); - -afterAll(async () => { - const result = await Projects.deleteMany({}); //clear the projects collection after tests are done - const result2 = await Users.deleteMany({ - _id: { $ne: '64f551e5b28d5292975e08c8' } - }); //clear the users collection after tests are done except for the mockdata user account - const result3 = await Sessions.deleteMany({ - cookieId: { $ne: '64f551e5b28d5292975e08c8' } - }); - await mongoose.connection.close(); -}); - -describe('Server endpoint tests', () => { - it('should pass this test request', async () => { - const response = await request(app).get('/test'); - expect(response.status).toBe(200); - expect(response.text).toBe('test request is working'); - }); - - // // test saveProject endpoint - // describe('/login', () => { - // describe('/POST', () => { - // it('responds with a status of 200 and json object equal to project sent', async () => { - // return request(app) - // .post('/login') - // .set('Cookie', [`ssid=${user.userId}`]) - // .set('Accept', 'application/json') - // .send(projectToSave) - // .expect(200) - // .expect('Content-Type', /application\/json/) - // .then((res) => expect(res.body.name).toBe(projectToSave.name)); - // }); - // // }); - // }); - // }); - - // test saveProject endpoint - describe('/saveProject', () => { - describe('POST', () => { - it('responds with a status of 200 and json object equal to project sent', async () => { - return request(app) - .post('/saveProject') - .set('Cookie', [`ssid=${user.userId}`]) - .set('Accept', 'application/json') - .send(projectToSave) - .expect(200) - .expect('Content-Type', /application\/json/) - .then((res) => expect(res.body.name).toBe(projectToSave.name)); - }); - // }); - }); - }); - // test getProjects endpoint - describe('/getProjects', () => { - describe('POST', () => { - it('responds with status of 200 and json object equal to an array of user projects', () => { - return request(app) - .post('/getProjects') - .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ userId: projectToSave.userId }) - .expect(200) - .expect('Content-Type', /json/) - .then((res) => { - expect(Array.isArray(res.body)).toBeTruthy; - expect(res.body[0].name).toBe(state.name); - }); - }); - }); - }); - // test deleteProject endpoint - describe('/deleteProject', () => { - describe('DELETE', () => { - it('responds with status of 200 and json object equal to deleted project', async () => { - const response: Response = await request(app) - .post('/getProjects') - .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ userId: projectToSave.userId }); - const _id: String = response.body[0]._id; - const userId: String = user.userId; - return request(app) - .delete('/deleteProject') - .set('Cookie', [`ssid=${user.userId}`]) - .set('Content-Type', 'application/json') - .send({ _id, userId }) - .expect(200) - .then((res) => expect(res.body._id).toBe(_id)); - }); - }); - }); - - //test publishProject endpoint - describe('/publishProject', () => { - describe('POST', () => { - it('responds with status of 200 and json object equal to published project', async () => { - const projObj = await request(app) - .post('/saveProject') - .set('Cookie', [`ssid=${user.userId}`]) - .set('Accept', 'application/json') - .send(projectToSave); - const _id: String = projObj.body._id; - const project: String = projObj.body.project; - const comments: String = projObj.body.comments; - const username: String = projObj.body.username; - const name: String = projObj.body.name; - const userId: String = user.userId; - return request(app) - .post('/publishProject') - .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name - .expect(200) - .then((res) => { - expect(res.body._id).toBe(_id); - expect(res.body.published).toBe(true); - }); - }); - it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { - const projObj: Response = await request(app) - .post('/getProjects') - .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ userId: projectToSave.userId }); - const _id: String = projObj.body[0]._id; - const project: String = projObj.body[0].project; - const comments: String = projObj.body[0].comments; - const username: String = projObj.body[0].username; - const name: String = projObj.body[0].name; - const userId: String = 'ERROR'; - return request(app) - .post('/publishProject') - .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name - .expect(500) - .then((res) => { - expect(res.body.err).not.toBeNull(); - }); - }); - it('responds with status of 500 and error if _id was not a valid mongo ObjectId', async () => { - const projObj: Response = await request(app) - .post('/getProjects') - .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ userId: projectToSave.userId }); - const _id: String = 'ERROR'; - const project: String = projObj.body[0].project; - const comments: String = projObj.body[0].comments; - const username: String = user.username; - const name: String = projObj.body[0].name; - const userId: String = user.userId; - - return request(app) - .post('/publishProject') - .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name - .expect(200) - .then((res) => { - expect(res.body._id).not.toEqual(_id); - }); - }); - }); - }); - - //test getMarketplaceProjects endpoint - describe('/getMarketplaceProjects', () => { - //most recent project should be the one from publishProject - - describe('GET', () => { - it('responds with status of 200 and json object equal to unpublished project', async () => { - return request(app) - .get('/getMarketplaceProjects') - .set('Content-Type', 'application/json') - .expect(200) - .then((res) => { - expect(Array.isArray(res.body)).toBe(true); - expect(res.body[0]._id).toBeTruthy; - }); - }); - }); - }); - - //test cloneProject endpoint - describe('/cloneProject/:docId', () => { - describe('GET', () => { - it('responds with status of 200 and json object equal to cloned project', async () => { - const projObj = await request(app) - .get('/getMarketplaceProjects') - .set('Content-Type', 'application/json'); - - return request(app) - .get(`/cloneProject/${projObj.body[0]._id}`) - .set('Cookie', [`ssid=${user.userId}`]) // Set the cookie - .query({ username: user.username }) - .expect(200) - .then((res) => { - expect(res.body.forked).toBeTruthy; - expect(res.body.username).toBe(user.username); - }); - }); - it('responds with status of 500 and error', async () => { - const projObj = await request(app) - .get('/getMarketplaceProjects') - .set('Content-Type', 'application/json'); - - return request(app) - .get(`/cloneProject/${projObj.body[0]._id}`) - .set('Cookie', [`ssid=${user.userId}`]) // Set the cookie - .query({ username: [] }) - .expect(500) - .then((res) => { - expect(res.body.err).not.toBeNull(); - }); - }); - }); - }); - - //test unpublishProject endpoint - describe('/unpublishProject', () => { - describe('PATCH', () => { - it('responds with status of 200 and json object equal to unpublished project', async () => { - const response: Response = await request(app) - .post('/getProjects') - .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ userId: projectToSave.userId }); //most recent project should be the one from publishProject - const _id: String = response.body[0]._id; - const userId: String = user.userId; - return request(app) - .patch('/unpublishProject') - .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, userId }) - .expect(200) - .then((res) => { - expect(res.body._id).toBe(_id); - expect(res.body.published).toBe(false); - }); - }); - it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { - const projObj: Response = await request(app) - .post('/getProjects') - .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ userId: projectToSave.userId }); - const _id: String = projObj.body[0]._id; - const project: String = projObj.body[0].project; - const comments: String = projObj.body[0].comments; - const username: String = projObj.body[0].username; - const name: String = projObj.body[0].name; - let userId: String = user.userId; - await request(app) //publishing a project first - .post('/publishProject') - .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name }); - - userId = 'ERROR'; - return request(app) - .patch('/unpublishProject') - .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, userId }) - .expect(500) - .then((res) => { - expect(res.body.err).not.toBeNull(); - }); - }); - it('responds with status of 500 and error if _id was not a string', async () => { - const userId: String = user.userId; - - return request(app) - .patch('/unpublishProject') - .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ userId }) - .expect(500) - .then((res) => { - expect(res.body.err).not.toBeNull(); - }); - }); - }); - }); -}); - -describe('SessionController tests', () => { - describe('isLoggedIn', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - // Mock Express request and response objects and next function - const mockReq: any = { - cookies: null, //trying to trigger if cookies was not assigned - body: { - userId: 'sampleUserId' // Set up a sample userId in the request body - } - }; - const mockRes: any = { - json: jest.fn(), - status: jest.fn(), - redirect: jest.fn() - }; - const next = jest.fn(); - it('Assign userId from request body to cookieId', async () => { - // Call isLoggedIn - await sessionController.isLoggedIn(mockReq, mockRes, next); - expect(mockRes.redirect).toHaveBeenCalledWith('/'); - // Ensure that next() was called - }); - it('Trigger a database query error for findOne', async () => { - const mockFindOne = jest - .spyOn(mongoose.model('Sessions'), 'findOne') - .mockImplementation(() => { - throw new Error('Database query error'); - }); - // Call isLoggedIn - await sessionController.isLoggedIn(mockReq, mockRes, next); - // Ensure that next() was called with the error - expect(next).toHaveBeenCalledWith( - expect.objectContaining({ - log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive - }) - ); - - mockFindOne.mockRestore(); - }); - }); - - describe('startSession', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - it('Trigger a database query error for findOne', async () => { - const mockReq: any = { - cookies: projectToSave.userId, //trying to trigger if cookies was not assigned - body: { - userId: 'sampleUserId' // Set up a sample userId in the request body - } - }; - const mockRes: any = { - json: jest.fn(), - status: jest.fn(), - redirect: jest.fn(), - locals: { id: projectToSave.userId } - }; - - const next = jest.fn(); - const findOneMock = jest.spyOn( - mongoose.model('Sessions'), - 'findOne' - ) as jest.Mock; - findOneMock.mockImplementation( - (query: any, callback: (err: any, ses: any) => void) => { - callback(new Error('Database query error'), null); - } - ); - // Call startSession - await sessionController.startSession(mockReq, mockRes, next); - // Check that next() was called with the error - expect(next).toHaveBeenCalledWith( - expect.objectContaining({ - log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive - }) - ); - - findOneMock.mockRestore(); - }); - - xit('Check if a new Session is created', async () => { - //not working for some reason cannot get mocknext() to be called in test? - - const mockReq: any = { - cookies: projectToSave.userId, //trying to trigger if cookies was not assigned - body: { - userId: 'sampleUserId' // Set up a sample userId in the request body - } - }; - const mockRes: any = { - json: jest.fn(), - status: jest.fn(), - redirect: jest.fn(), - locals: { id: 'testID' } //a sesion id that doesnt exist - }; - - const mockNext = jest.fn(); - - //Call startSession - // Wrap your test logic in an async function - await sessionController.startSession(mockReq, mockRes, mockNext); - - //check if it reaches next() - //await expect(mockRes.locals.ssid).toBe('testID'); - expect(mockNext).toHaveBeenCalled(); - }); - }); -}); - -// describe('marketplaceController Middleware', () => { -// describe('getProjects tests', () => { -// it('should add the projects as an array to res.locals', () => { -// const req = {}; -// const res = { locals: {} }; -// console.log(marketplaceController.getPublishedProjects); -// console.log(typeof marketplaceController.getPublishedProjects); -// marketplaceController.getPublishedProjects(req, res, mockNext); -// expect(Array.isArray(res.locals.publishedProjects)).toBe(true); -// expect(mockNext).toHaveBeenCalled(); -// }); -// }); - -// it('should send an error response if there is an error in the middleware', () => { -// const req = { user: { isAuthenticated: false } }; -// const res = mockResponse(); - -// marketplaceController.authenticateMiddleware(req, res, mockNext); - -// expect(res.status).toHaveBeenCalledWith(500); -// expect(res.json).toHaveBeenCalledWith({ err: 'Error in marketplaceController.getPublishedProjects, check server logs for details' }); -// }); -// }); +/** + * @jest-environment node + */ + +import marketplaceController from '../server/controllers/marketplaceController'; +import sessionController from '../server/controllers/sessionController'; +import app from '../server/server'; +import mockData from '../mockData'; +import { profileEnd } from 'console'; +import { Projects, Users, Sessions } from '../server/models/reactypeModels'; +const request = require('supertest'); +const mongoose = require('mongoose'); +const mockNext = jest.fn(); // Mock nextFunction +const MONGO_DB = import.meta.env.MONGO_DB_TEST; +const { state, projectToSave, user } = mockData; +const PORT = 8080; + +beforeAll(async () => { + await mongoose.connect(MONGO_DB, { + useNewUrlParser: true, + useUnifiedTopology: true + }); +}); + +afterAll(async () => { + const result = await Projects.deleteMany({}); //clear the projects collection after tests are done + const result2 = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result3 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); + await mongoose.connection.close(); +}); + +describe('Server endpoint tests', () => { + it('should pass this test request', async () => { + const response = await request(app).get('/test'); + expect(response.status).toBe(200); + expect(response.text).toBe('test request is working'); + }); + + // // test saveProject endpoint + // describe('/login', () => { + // describe('/POST', () => { + // it('responds with a status of 200 and json object equal to project sent', async () => { + // return request(app) + // .post('/login') + // .set('Cookie', [`ssid=${user.userId}`]) + // .set('Accept', 'application/json') + // .send(projectToSave) + // .expect(200) + // .expect('Content-Type', /application\/json/) + // .then((res) => expect(res.body.name).toBe(projectToSave.name)); + // }); + // // }); + // }); + // }); + + // test saveProject endpoint + describe('/saveProject', () => { + describe('POST', () => { + it('responds with a status of 200 and json object equal to project sent', async () => { + return request(app) + .post('/saveProject') + .set('Cookie', [`ssid=${user.userId}`]) + .set('Accept', 'application/json') + .send(projectToSave) + .expect(200) + .expect('Content-Type', /application\/json/) + .then((res) => expect(res.body.name).toBe(projectToSave.name)); + }); + // }); + }); + }); + // test getProjects endpoint + describe('/getProjects', () => { + describe('POST', () => { + it('responds with status of 200 and json object equal to an array of user projects', () => { + return request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }) + .expect(200) + .expect('Content-Type', /json/) + .then((res) => { + expect(Array.isArray(res.body)).toBeTruthy; + expect(res.body[0].name).toBe(state.name); + }); + }); + }); + }); + // test deleteProject endpoint + describe('/deleteProject', () => { + describe('DELETE', () => { + it('responds with status of 200 and json object equal to deleted project', async () => { + const response: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); + const _id: String = response.body[0]._id; + const userId: String = user.userId; + return request(app) + .delete('/deleteProject') + .set('Cookie', [`ssid=${user.userId}`]) + .set('Content-Type', 'application/json') + .send({ _id, userId }) + .expect(200) + .then((res) => expect(res.body._id).toBe(_id)); + }); + }); + }); + + //test publishProject endpoint + describe('/publishProject', () => { + describe('POST', () => { + it('responds with status of 200 and json object equal to published project', async () => { + const projObj = await request(app) + .post('/saveProject') + .set('Cookie', [`ssid=${user.userId}`]) + .set('Accept', 'application/json') + .send(projectToSave); + const _id: String = projObj.body._id; + const project: String = projObj.body.project; + const comments: String = projObj.body.comments; + const username: String = projObj.body.username; + const name: String = projObj.body.name; + const userId: String = user.userId; + return request(app) + .post('/publishProject') + .set('Content-Type', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name + .expect(200) + .then((res) => { + expect(res.body._id).toBe(_id); + expect(res.body.published).toBe(true); + }); + }); + it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); + const _id: String = projObj.body[0]._id; + const project: String = projObj.body[0].project; + const comments: String = projObj.body[0].comments; + const username: String = projObj.body[0].username; + const name: String = projObj.body[0].name; + const userId: String = 'ERROR'; + return request(app) + .post('/publishProject') + .set('Content-Type', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name + .expect(500) + .then((res) => { + expect(res.body.err).not.toBeNull(); + }); + }); + it('responds with status of 500 and error if _id was not a valid mongo ObjectId', async () => { + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); + const _id: String = 'ERROR'; + const project: String = projObj.body[0].project; + const comments: String = projObj.body[0].comments; + const username: String = user.username; + const name: String = projObj.body[0].name; + const userId: String = user.userId; + + return request(app) + .post('/publishProject') + .set('Content-Type', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name + .expect(200) + .then((res) => { + expect(res.body._id).not.toEqual(_id); + }); + }); + }); + }); + + //test getMarketplaceProjects endpoint + describe('/getMarketplaceProjects', () => { + //most recent project should be the one from publishProject + + describe('GET', () => { + it('responds with status of 200 and json object equal to unpublished project', async () => { + return request(app) + .get('/getMarketplaceProjects') + .set('Content-Type', 'application/json') + .expect(200) + .then((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body[0]._id).toBeTruthy; + }); + }); + }); + }); + + //test cloneProject endpoint + describe('/cloneProject/:docId', () => { + describe('GET', () => { + it('responds with status of 200 and json object equal to cloned project', async () => { + const projObj = await request(app) + .get('/getMarketplaceProjects') + .set('Content-Type', 'application/json'); + + return request(app) + .get(`/cloneProject/${projObj.body[0]._id}`) + .set('Cookie', [`ssid=${user.userId}`]) // Set the cookie + .query({ username: user.username }) + .expect(200) + .then((res) => { + expect(res.body.forked).toBeTruthy; + expect(res.body.username).toBe(user.username); + }); + }); + it('responds with status of 500 and error', async () => { + const projObj = await request(app) + .get('/getMarketplaceProjects') + .set('Content-Type', 'application/json'); + + return request(app) + .get(`/cloneProject/${projObj.body[0]._id}`) + .set('Cookie', [`ssid=${user.userId}`]) // Set the cookie + .query({ username: [] }) + .expect(500) + .then((res) => { + expect(res.body.err).not.toBeNull(); + }); + }); + }); + }); + + //test unpublishProject endpoint + describe('/unpublishProject', () => { + describe('PATCH', () => { + it('responds with status of 200 and json object equal to unpublished project', async () => { + const response: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); //most recent project should be the one from publishProject + const _id: String = response.body[0]._id; + const userId: String = user.userId; + return request(app) + .patch('/unpublishProject') + .set('Content-Type', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, userId }) + .expect(200) + .then((res) => { + expect(res.body._id).toBe(_id); + expect(res.body.published).toBe(false); + }); + }); + it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); + const _id: String = projObj.body[0]._id; + const project: String = projObj.body[0].project; + const comments: String = projObj.body[0].comments; + const username: String = projObj.body[0].username; + const name: String = projObj.body[0].name; + let userId: String = user.userId; + await request(app) //publishing a project first + .post('/publishProject') + .set('Content-Type', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }); + + userId = 'ERROR'; + return request(app) + .patch('/unpublishProject') + .set('Content-Type', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, userId }) + .expect(500) + .then((res) => { + expect(res.body.err).not.toBeNull(); + }); + }); + it('responds with status of 500 and error if _id was not a string', async () => { + const userId: String = user.userId; + + return request(app) + .patch('/unpublishProject') + .set('Content-Type', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId }) + .expect(500) + .then((res) => { + expect(res.body.err).not.toBeNull(); + }); + }); + }); + }); +}); + +describe('SessionController tests', () => { + describe('isLoggedIn', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + // Mock Express request and response objects and next function + const mockReq: any = { + cookies: null, //trying to trigger if cookies was not assigned + body: { + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; + const mockRes: any = { + json: jest.fn(), + status: jest.fn(), + redirect: jest.fn() + }; + const next = jest.fn(); + it('Assign userId from request body to cookieId', async () => { + // Call isLoggedIn + await sessionController.isLoggedIn(mockReq, mockRes, next); + expect(mockRes.redirect).toHaveBeenCalledWith('/'); + // Ensure that next() was called + }); + it('Trigger a database query error for findOne', async () => { + const mockFindOne = jest + .spyOn(mongoose.model('Sessions'), 'findOne') + .mockImplementation(() => { + throw new Error('Database query error'); + }); + // Call isLoggedIn + await sessionController.isLoggedIn(mockReq, mockRes, next); + // Ensure that next() was called with the error + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive + }) + ); + + mockFindOne.mockRestore(); + }); + }); + + describe('startSession', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('Trigger a database query error for findOne', async () => { + const mockReq: any = { + cookies: projectToSave.userId, //trying to trigger if cookies was not assigned + body: { + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; + const mockRes: any = { + json: jest.fn(), + status: jest.fn(), + redirect: jest.fn(), + locals: { id: projectToSave.userId } + }; + + const next = jest.fn(); + const findOneMock = jest.spyOn( + mongoose.model('Sessions'), + 'findOne' + ) as jest.Mock; + findOneMock.mockImplementation( + (query: any, callback: (err: any, ses: any) => void) => { + callback(new Error('Database query error'), null); + } + ); + // Call startSession + await sessionController.startSession(mockReq, mockRes, next); + // Check that next() was called with the error + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive + }) + ); + + findOneMock.mockRestore(); + }); + + xit('Check if a new Session is created', async () => { + //not working for some reason cannot get mocknext() to be called in test? + + const mockReq: any = { + cookies: projectToSave.userId, //trying to trigger if cookies was not assigned + body: { + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; + const mockRes: any = { + json: jest.fn(), + status: jest.fn(), + redirect: jest.fn(), + locals: { id: 'testID' } //a sesion id that doesnt exist + }; + + const mockNext = jest.fn(); + + //Call startSession + // Wrap your test logic in an async function + await sessionController.startSession(mockReq, mockRes, mockNext); + + //check if it reaches next() + //await expect(mockRes.locals.ssid).toBe('testID'); + expect(mockNext).toHaveBeenCalled(); + }); + }); +}); + +// describe('marketplaceController Middleware', () => { +// describe('getProjects tests', () => { +// it('should add the projects as an array to res.locals', () => { +// const req = {}; +// const res = { locals: {} }; +// console.log(marketplaceController.getPublishedProjects); +// console.log(typeof marketplaceController.getPublishedProjects); +// marketplaceController.getPublishedProjects(req, res, mockNext); +// expect(Array.isArray(res.locals.publishedProjects)).toBe(true); +// expect(mockNext).toHaveBeenCalled(); +// }); +// }); + +// it('should send an error response if there is an error in the middleware', () => { +// const req = { user: { isAuthenticated: false } }; +// const res = mockResponse(); + +// marketplaceController.authenticateMiddleware(req, res, mockNext); + +// expect(res.status).toHaveBeenCalledWith(500); +// expect(res.json).toHaveBeenCalledWith({ err: 'Error in marketplaceController.getPublishedProjects, check server logs for details' }); +// }); +// }); diff --git a/__tests__/signIn.test.tsx b/__tests__/signIn.test.tsx index 50cb5f66f..c40ca9132 100644 --- a/__tests__/signIn.test.tsx +++ b/__tests__/signIn.test.tsx @@ -1,51 +1,51 @@ -import SignIn from '../app/src/components/login/SignIn'; -import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import store from '../app/src/redux/store'; -import { BrowserRouter } from 'react-router-dom'; -import '@testing-library/jest-dom'; - -function TestSignIn() { - return ( - - - - - - ); -} - -describe('sign in page', () => { - test('should render a login input', () => { - render(); - expect(screen.getByTestId('username-input')).toBeInTheDocument(); - }); - test('should render a password field', () => { - render(); - expect(screen.getByTestId('password-input')).toBeInTheDocument(); - }); - test('should render 4 login buttons', () => { - render(); - expect(screen.getAllByRole('button')).toHaveLength(4); - }); - test('should invalidate empty username field', () => { - render(); - fireEvent.click(screen.getAllByRole('button')[1]); - waitFor(() => { - expect(screen.getByText('No Username Input')).toBeInTheDocument(); - }); - }); - test('should invalidate empty password field', () => { - render(); - fireEvent.change(screen.getByRole('textbox'), { - target: { - value: 'username' - } - }); - fireEvent.click(screen.getAllByRole('button')[1]); - waitFor(() => { - expect(screen.getByText('No Password Input')).toBeInTheDocument(); - }); - }); -}); +import SignIn from '../app/src/components/login/SignIn'; +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import store from '../app/src/redux/store'; +import { BrowserRouter } from 'react-router-dom'; +import '@testing-library/jest-dom'; + +function TestSignIn() { + return ( + + + + + + ); +} + +describe('sign in page', () => { + test('should render a login input', () => { + render(); + expect(screen.getByTestId('username-input')).toBeInTheDocument(); + }); + test('should render a password field', () => { + render(); + expect(screen.getByTestId('password-input')).toBeInTheDocument(); + }); + test('should render 4 login buttons', () => { + render(); + expect(screen.getAllByRole('button')).toHaveLength(4); + }); + test('should invalidate empty username field', () => { + render(); + fireEvent.click(screen.getAllByRole('button')[1]); + waitFor(() => { + expect(screen.getByText('No Username Input')).toBeInTheDocument(); + }); + }); + test('should invalidate empty password field', () => { + render(); + fireEvent.change(screen.getByRole('textbox'), { + target: { + value: 'username' + } + }); + fireEvent.click(screen.getAllByRole('button')[1]); + waitFor(() => { + expect(screen.getByText('No Password Input')).toBeInTheDocument(); + }); + }); +}); diff --git a/__tests__/spec.ts b/__tests__/spec.ts index fe98fd3b4..a8fd4940d 100644 --- a/__tests__/spec.ts +++ b/__tests__/spec.ts @@ -1,32 +1,32 @@ -import 'regenerator-runtime/runtime'; // if there is an error with moduleNameMapper, npm -S install regenerator-runtime - -//const { Application } = require('spectron'); -// const electronPath = require('electron'); -// const path = require('path'); - -// let app; - -// // beforeAll(() => { -// // // create a new app to test with setTimeout to be 15000 because the app takes a few seconds to spin up -// // app = new Application({ -// // path: electronPath, -// // chromeDriverArgs: ['--disable-extensions'], -// // args: [path.join(__dirname, '../app/electron/main.js')] // this is the path from this test file to main.js inside electron folder -// // }); -// // return app.start(); -// // }, 15000); - -// // getWindowsCount() will return 2 instead of 1 in dev mode (one for the actual app, one in the browser at localhost:8080 in dev mode) -// xtest('Displays App window', async () => { -// const windowCount = await app.client.getWindowCount(); -// // expect(windowCount).toBe(1); // this returns true/passed if in production mode, change mode in script "test" to 'production' instead of 'test' -// expect(windowCount).toBe(2); // 'dev' or 'test' mode results in 2 windows (one for the app and one for the browser) -// }); - -// /* we want to test other functionalities of app.client such as text, title, etc. but even the examples from the official spectron website -// or github repo did not yield the same outcomes as demonstrated. So we stopped testing Electron app here */ -// // afterAll(() => { -// // if (app && app.isRunning()) { -// // return app.stop(); -// // } -// // }); +import 'regenerator-runtime/runtime'; // if there is an error with moduleNameMapper, npm -S install regenerator-runtime + +//const { Application } = require('spectron'); +// const electronPath = require('electron'); +// const path = require('path'); + +// let app; + +// // beforeAll(() => { +// // // create a new app to test with setTimeout to be 15000 because the app takes a few seconds to spin up +// // app = new Application({ +// // path: electronPath, +// // chromeDriverArgs: ['--disable-extensions'], +// // args: [path.join(__dirname, '../app/electron/main.js')] // this is the path from this test file to main.js inside electron folder +// // }); +// // return app.start(); +// // }, 15000); + +// // getWindowsCount() will return 2 instead of 1 in dev mode (one for the actual app, one in the browser at localhost:8080 in dev mode) +// xtest('Displays App window', async () => { +// const windowCount = await app.client.getWindowCount(); +// // expect(windowCount).toBe(1); // this returns true/passed if in production mode, change mode in script "test" to 'production' instead of 'test' +// expect(windowCount).toBe(2); // 'dev' or 'test' mode results in 2 windows (one for the app and one for the browser) +// }); + +// /* we want to test other functionalities of app.client such as text, title, etc. but even the examples from the official spectron website +// or github repo did not yield the same outcomes as demonstrated. So we stopped testing Electron app here */ +// // afterAll(() => { +// // if (app && app.isRunning()) { +// // return app.stop(); +// // } +// // }); diff --git a/__tests__/stateManagementReducer.test.js b/__tests__/stateManagementReducer.test.js index 3b6d5e755..6a9045c28 100644 --- a/__tests__/stateManagementReducer.test.js +++ b/__tests__/stateManagementReducer.test.js @@ -1,309 +1,309 @@ -import reducer from '../app/src/redux/reducers/slice/appStateSlice'; -import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; - -//initializing copy of initial state to be used for test suite -let state = JSON.parse(JSON.stringify(initialState)); -state.components = [ - { - id: 1, - name: 'App', - style: {}, - code: "import React, { useState, useEffect, useContext} from 'react';\n\n\n\nimport C1 from './C1'\nconst App = (props) => {\n\n\n const [appState, setAppState] = useState(1);\n\n return(\n <>\n\n \n );\n}\n\nexport default App\n", - children: [ - { - type: 'HTML Element', - typeId: 1000, - name: 'separator', - childId: 1000, - style: { border: 'none' }, - attributes: {}, - children: [] - }, - { - type: 'Component', - typeId: 2, - name: 'C1', - childId: 1, - style: {}, - attributes: {}, - children: [], - stateProps: [], - passedInProps: [] - } - ], - isPage: true, - past: [[]], - future: [], - stateProps: [], - useStateCodes: [ - 'const [appState, setAppState] = useState(1)' - ] - }, - { - id: 2, - name: 'C1', - nextChildId: 1, - style: {}, - attributes: {}, - code: "import React, { useState, useEffect, useContext} from 'react';\n\n\n\n\nconst C1 = (props) => {\n\n\n\n return(\n <>\n\n \n );\n}\n\nexport default C1\n", - children: [], - isPage: false, - past: [], - future: [], - stateProps: [], - useStateCodes: [], - passedInProps: [] - } -]; - -const findComponent = (components, componentId) => { - return components.find((elem) => elem.id === componentId); -}; - -describe('stateManagementReducer test', () => { - // TEST 'ADD STATE' - describe('addState', () => { - // setting canvas focus to root component (App) - // state.canvasFocus.componentId = 1; - // action dispatched to be tested - const action1 = { - type: 'appState/addState', - payload: { - newState: { - id: 'App-testAppState', - key: 'testAppState', - type: 'number', - value: 1 - }, - setNewState: { - id: 'App-setTestAppState', - key: 'setTestAppState', - type: 'func', - value: '' - }, - contextParam: { - allContext: [] - } - } - }; - - // setting test state - state = reducer(state, action1); - let currComponent = findComponent(state.components, 1); - - it('should add state and its setter function to the stateProps array of the current component', () => { - expect(currComponent.stateProps.length).toEqual(2); - }); - it(`state id should be 'App-testAppState'`, () => { - expect(currComponent.stateProps[0].id).toEqual('App-testAppState'); - }); - it(`state key should be 'testAppState'`, () => { - expect(currComponent.stateProps[0].key).toEqual('testAppState'); - }); - it(`state value should be 1`, () => { - expect(currComponent.stateProps[0].value).toEqual(1); - }); - it(`state value type should be 'number'`, () => { - expect(currComponent.stateProps[0].type).toEqual('number'); - }); - it(`function state id should be 'App-setTestAppState'`, () => { - expect(currComponent.stateProps[1].id).toEqual('App-setTestAppState'); - }); - it(`function state key should be 'setTestAppState'`, () => { - expect(currComponent.stateProps[1].key).toEqual('setTestAppState'); - }); - it(`function state value should be blank`, () => { - expect(currComponent.stateProps[1].value).toEqual(''); - }); - it(`function state key should be func`, () => { - expect(currComponent.stateProps[1].type).toEqual('func'); - }); - - const action2 = { - type: 'appState/addState', - payload: { - newState: { - id: 'App-testAppState2', - key: 'isLoggedIn', - type: 'boolean', - value: 'false' - }, - setNewState: { - id: 'App-setTestAppState2', - key: 'setIsLoggedIn', - type: 'func', - value: '' - }, - contextParam: { - allContext: [] - } - } - }; - - state = reducer(state, action2); - - describe('should handle value with type of boolean', () => { - it(`state key type should be boolean`, () => { - expect(state.components[0].stateProps[2].type).toEqual('boolean'); - }); - it(`state value should be false`, () => { - expect(state.components[0].stateProps[2].value).toEqual('false'); - }); - }); - }); - - // TEST 'ADD PASSEDINPROPS' - describe('addPassedInProps', () => { - state = JSON.parse(JSON.stringify(state)); - // setting canvas focus to the child component - state.canvasFocus.componentId = 2; - - // action dispatched to be tested - const action = { - type: 'appState/addPassedInProps', - payload: { - passedInProps: { - id: 'App-testAppState', - key: 'testAppState', - type: 'number', - value: 1 - }, - contextParam: { - allContext: [] - } - } - }; - - // setting test state - state = reducer(state, action); - const currComponent = findComponent(state.components, 2); - const parentComponent = findComponent(state.components, 1); - - it(`current component should have a state id: 'App-testAppState' `, () => { - expect(currComponent.passedInProps[0].id).toEqual('App-testAppState'); - }); - it(`current component should have a state key: 'testAppState' `, () => { - expect(currComponent.passedInProps[0].key).toEqual('testAppState'); - }); - it(`current component should have a state value equal to 1`, () => { - expect(currComponent.passedInProps[0].value).toEqual(1); - }); - it(`current component should have a state value type: 'number'`, () => { - expect(currComponent.passedInProps[0].type).toEqual('number'); - }); - //check parent children array to make sure it is being added here as well - it(`parent component 'passedInProps' array length should be 1`, () => { - expect(currComponent.passedInProps.length).toEqual(1); - }); - it(`parent component should have a state id: 'App-testAppState' `, () => { - expect(parentComponent.children[1].passedInProps[0].id).toEqual( - 'App-testAppState' - ); - }); - it(`parent component should have a state key: 'testAppState' `, () => { - expect(parentComponent.children[1].passedInProps[0].key).toEqual( - 'testAppState' - ); - }); - it(`parent component should have a state value equal to 1`, () => { - expect(parentComponent.children[1].passedInProps[0].value).toEqual(1); - }); - it(`parent component should have a state value type: 'number'`, () => { - expect(parentComponent.children[1].passedInProps[0].type).toEqual( - 'number' - ); - }); - }); - - // TEST 'DELETE PASSEDINPROPS' - describe('deletePassedInProps', () => { - it('should delete the state passed down from parent component in the child component', () => { - // setting canvas focus to the child component - state = JSON.parse(JSON.stringify(state)); - state.canvasFocus.componentId = 2; - - // action dispatched to be tested - const action = { - type: 'appState/deletePassedInProps', - payload: { - rowId: 'App-testAppState', - contextParam: { - allContext: [] - } - } - }; - - // setting test state - state = reducer(state, action); - const parentComponent = findComponent(state.components, 1); - const currComponent = findComponent(state.components, 2); - - expect(currComponent.passedInProps.length).toEqual(0); - expect(parentComponent.children[1].passedInProps.length).toEqual(0); // need to fix reducer - }); - }); - - // TEST 'DELETE STATE' - describe('deleteState', () => { - it('should delete all instances of state from stateProps and passedInProps', () => { - // setting canvas focus to root component - state = JSON.parse(JSON.stringify(state)); - state.canvasFocus.componentId = 1; - - // action dispatched to be tested - const action = { - type: 'appState/deleteState', - payload: { - stateProps: [], - rowId: 'App-appState', - otherId: 'App-setAppState', - contextParam: { - allContext: [] - } - } - }; - - // setting intial test state - let parentComponent = findComponent(state.components, 1); - parentComponent.children[1].passedInProps = [ - { - id: 'App-appState', - key: 'appState', - type: 'number', - value: 1 - }, - { - id: 'App-setAppState', - key: 'setAppState', - type: 'func', - value: '' - } - ]; - - let childComponent = findComponent(state.components, 2); - childComponent.passedInProps = [ - { - id: 'App-appState', - key: 'appState', - type: 'number', - value: 1 - }, - { - id: 'App-setAppState', - key: 'setAppState', - type: 'func', - value: '' - } - ]; - - // updating components after state updates - state = reducer(state, action); - parentComponent = findComponent(state.components, 1); - childComponent = findComponent(state.components, 2); - - expect(childComponent.passedInProps.length).toEqual(0); - expect(parentComponent.stateProps.length).toEqual(0); - expect(parentComponent.children[1].passedInProps.length).toEqual(0); - }); - }); -}); +import reducer from '../app/src/redux/reducers/slice/appStateSlice'; +import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; + +//initializing copy of initial state to be used for test suite +let state = JSON.parse(JSON.stringify(initialState)); +state.components = [ + { + id: 1, + name: 'App', + style: {}, + code: "import React, { useState, useEffect, useContext} from 'react';\n\n\n\nimport C1 from './C1'\nconst App = (props) => {\n\n\n const [appState, setAppState] = useState(1);\n\n return(\n <>\n\n \n );\n}\n\nexport default App\n", + children: [ + { + type: 'HTML Element', + typeId: 1000, + name: 'separator', + childId: 1000, + style: { border: 'none' }, + attributes: {}, + children: [] + }, + { + type: 'Component', + typeId: 2, + name: 'C1', + childId: 1, + style: {}, + attributes: {}, + children: [], + stateProps: [], + passedInProps: [] + } + ], + isPage: true, + past: [[]], + future: [], + stateProps: [], + useStateCodes: [ + 'const [appState, setAppState] = useState(1)' + ] + }, + { + id: 2, + name: 'C1', + nextChildId: 1, + style: {}, + attributes: {}, + code: "import React, { useState, useEffect, useContext} from 'react';\n\n\n\n\nconst C1 = (props) => {\n\n\n\n return(\n <>\n\n \n );\n}\n\nexport default C1\n", + children: [], + isPage: false, + past: [], + future: [], + stateProps: [], + useStateCodes: [], + passedInProps: [] + } +]; + +const findComponent = (components, componentId) => { + return components.find((elem) => elem.id === componentId); +}; + +describe('stateManagementReducer test', () => { + // TEST 'ADD STATE' + describe('addState', () => { + // setting canvas focus to root component (App) + // state.canvasFocus.componentId = 1; + // action dispatched to be tested + const action1 = { + type: 'appState/addState', + payload: { + newState: { + id: 'App-testAppState', + key: 'testAppState', + type: 'number', + value: 1 + }, + setNewState: { + id: 'App-setTestAppState', + key: 'setTestAppState', + type: 'func', + value: '' + }, + contextParam: { + allContext: [] + } + } + }; + + // setting test state + state = reducer(state, action1); + let currComponent = findComponent(state.components, 1); + + it('should add state and its setter function to the stateProps array of the current component', () => { + expect(currComponent.stateProps.length).toEqual(2); + }); + it(`state id should be 'App-testAppState'`, () => { + expect(currComponent.stateProps[0].id).toEqual('App-testAppState'); + }); + it(`state key should be 'testAppState'`, () => { + expect(currComponent.stateProps[0].key).toEqual('testAppState'); + }); + it(`state value should be 1`, () => { + expect(currComponent.stateProps[0].value).toEqual(1); + }); + it(`state value type should be 'number'`, () => { + expect(currComponent.stateProps[0].type).toEqual('number'); + }); + it(`function state id should be 'App-setTestAppState'`, () => { + expect(currComponent.stateProps[1].id).toEqual('App-setTestAppState'); + }); + it(`function state key should be 'setTestAppState'`, () => { + expect(currComponent.stateProps[1].key).toEqual('setTestAppState'); + }); + it(`function state value should be blank`, () => { + expect(currComponent.stateProps[1].value).toEqual(''); + }); + it(`function state key should be func`, () => { + expect(currComponent.stateProps[1].type).toEqual('func'); + }); + + const action2 = { + type: 'appState/addState', + payload: { + newState: { + id: 'App-testAppState2', + key: 'isLoggedIn', + type: 'boolean', + value: 'false' + }, + setNewState: { + id: 'App-setTestAppState2', + key: 'setIsLoggedIn', + type: 'func', + value: '' + }, + contextParam: { + allContext: [] + } + } + }; + + state = reducer(state, action2); + + describe('should handle value with type of boolean', () => { + it(`state key type should be boolean`, () => { + expect(state.components[0].stateProps[2].type).toEqual('boolean'); + }); + it(`state value should be false`, () => { + expect(state.components[0].stateProps[2].value).toEqual('false'); + }); + }); + }); + + // TEST 'ADD PASSEDINPROPS' + describe('addPassedInProps', () => { + state = JSON.parse(JSON.stringify(state)); + // setting canvas focus to the child component + state.canvasFocus.componentId = 2; + + // action dispatched to be tested + const action = { + type: 'appState/addPassedInProps', + payload: { + passedInProps: { + id: 'App-testAppState', + key: 'testAppState', + type: 'number', + value: 1 + }, + contextParam: { + allContext: [] + } + } + }; + + // setting test state + state = reducer(state, action); + const currComponent = findComponent(state.components, 2); + const parentComponent = findComponent(state.components, 1); + + it(`current component should have a state id: 'App-testAppState' `, () => { + expect(currComponent.passedInProps[0].id).toEqual('App-testAppState'); + }); + it(`current component should have a state key: 'testAppState' `, () => { + expect(currComponent.passedInProps[0].key).toEqual('testAppState'); + }); + it(`current component should have a state value equal to 1`, () => { + expect(currComponent.passedInProps[0].value).toEqual(1); + }); + it(`current component should have a state value type: 'number'`, () => { + expect(currComponent.passedInProps[0].type).toEqual('number'); + }); + //check parent children array to make sure it is being added here as well + it(`parent component 'passedInProps' array length should be 1`, () => { + expect(currComponent.passedInProps.length).toEqual(1); + }); + it(`parent component should have a state id: 'App-testAppState' `, () => { + expect(parentComponent.children[1].passedInProps[0].id).toEqual( + 'App-testAppState' + ); + }); + it(`parent component should have a state key: 'testAppState' `, () => { + expect(parentComponent.children[1].passedInProps[0].key).toEqual( + 'testAppState' + ); + }); + it(`parent component should have a state value equal to 1`, () => { + expect(parentComponent.children[1].passedInProps[0].value).toEqual(1); + }); + it(`parent component should have a state value type: 'number'`, () => { + expect(parentComponent.children[1].passedInProps[0].type).toEqual( + 'number' + ); + }); + }); + + // TEST 'DELETE PASSEDINPROPS' + describe('deletePassedInProps', () => { + it('should delete the state passed down from parent component in the child component', () => { + // setting canvas focus to the child component + state = JSON.parse(JSON.stringify(state)); + state.canvasFocus.componentId = 2; + + // action dispatched to be tested + const action = { + type: 'appState/deletePassedInProps', + payload: { + rowId: 'App-testAppState', + contextParam: { + allContext: [] + } + } + }; + + // setting test state + state = reducer(state, action); + const parentComponent = findComponent(state.components, 1); + const currComponent = findComponent(state.components, 2); + + expect(currComponent.passedInProps.length).toEqual(0); + expect(parentComponent.children[1].passedInProps.length).toEqual(0); // need to fix reducer + }); + }); + + // TEST 'DELETE STATE' + describe('deleteState', () => { + it('should delete all instances of state from stateProps and passedInProps', () => { + // setting canvas focus to root component + state = JSON.parse(JSON.stringify(state)); + state.canvasFocus.componentId = 1; + + // action dispatched to be tested + const action = { + type: 'appState/deleteState', + payload: { + stateProps: [], + rowId: 'App-appState', + otherId: 'App-setAppState', + contextParam: { + allContext: [] + } + } + }; + + // setting intial test state + let parentComponent = findComponent(state.components, 1); + parentComponent.children[1].passedInProps = [ + { + id: 'App-appState', + key: 'appState', + type: 'number', + value: 1 + }, + { + id: 'App-setAppState', + key: 'setAppState', + type: 'func', + value: '' + } + ]; + + let childComponent = findComponent(state.components, 2); + childComponent.passedInProps = [ + { + id: 'App-appState', + key: 'appState', + type: 'number', + value: 1 + }, + { + id: 'App-setAppState', + key: 'setAppState', + type: 'func', + value: '' + } + ]; + + // updating components after state updates + state = reducer(state, action); + parentComponent = findComponent(state.components, 1); + childComponent = findComponent(state.components, 2); + + expect(childComponent.passedInProps.length).toEqual(0); + expect(parentComponent.stateProps.length).toEqual(0); + expect(parentComponent.children[1].passedInProps.length).toEqual(0); + }); + }); +}); diff --git a/__tests__/tree.test.tsx b/__tests__/tree.test.tsx index 395e63da8..57215fc3b 100644 --- a/__tests__/tree.test.tsx +++ b/__tests__/tree.test.tsx @@ -1,113 +1,113 @@ -import TreeChart from '../app/src/tree/TreeChart'; -import React from 'react'; -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; -import { Provider } from 'react-redux'; -import store from '../app/src/redux/store'; -import 'd3'; - -let state = JSON.parse(JSON.stringify(initialState)); -state.components = [ - { - id: 1, - name: 'index', - style: {}, - code: `import React, { useState } from 'react'; - import A from '../components/A'; - import B from '../components/B'; - import Head from 'next/head'; - const index = (props): JSX.Element => { - const [value, setValue] = useState('INITIAL VALUE'); - return ( - <> - - index - - - - ); - }; - export default index; - `, - children: [ - { - childId: 1, - children: [ - { - childId: 2, - children: [], - name: 'A', - style: {}, - type: 'Component', - typeId: 2 - } - ], - name: 'div', - style: {}, - type: 'HTML Element', - typeId: 11 - }, - { - childId: 3, - children: [ - { - childId: 4, - children: [], - name: 'B', - style: {}, - type: 'Component', - typeId: 3 - } - ], - name: 'div', - style: {}, - type: 'HTML Element', - typeId: 11 - } - ], - isPage: true - }, - { - id: 2, - nextChildId: 1, - name: 'A', - style: {}, - code: '', - children: [], - isPage: false - }, - { - id: 3, - nextChildId: 1, - name: 'B', - style: {}, - code: '', - children: [], - isPage: false - } -]; - -// renders a tree of the components in tester -describe('Component Tree Render Test', () => { - test('should render full component tree based on state', () => { - render( - - - - ); - // elements that are not separators should appear in the tree - expect(screen.getByText('index')).toBeInTheDocument(); - expect(screen.getByText('A')).toBeInTheDocument(); - expect(screen.getByText('B')).toBeInTheDocument(); - // tree should not include separators - expect(screen.queryByText('separator')).toBe(null); - }); -}); +import TreeChart from '../app/src/tree/TreeChart'; +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { initialState } from '../app/src/redux/reducers/slice/appStateSlice'; +import { Provider } from 'react-redux'; +import store from '../app/src/redux/store'; +import 'd3'; + +let state = JSON.parse(JSON.stringify(initialState)); +state.components = [ + { + id: 1, + name: 'index', + style: {}, + code: `import React, { useState } from 'react'; + import A from '../components/A'; + import B from '../components/B'; + import Head from 'next/head'; + const index = (props): JSX.Element => { + const [value, setValue] = useState('INITIAL VALUE'); + return ( + <> + + index + + + + ); + }; + export default index; + `, + children: [ + { + childId: 1, + children: [ + { + childId: 2, + children: [], + name: 'A', + style: {}, + type: 'Component', + typeId: 2 + } + ], + name: 'div', + style: {}, + type: 'HTML Element', + typeId: 11 + }, + { + childId: 3, + children: [ + { + childId: 4, + children: [], + name: 'B', + style: {}, + type: 'Component', + typeId: 3 + } + ], + name: 'div', + style: {}, + type: 'HTML Element', + typeId: 11 + } + ], + isPage: true + }, + { + id: 2, + nextChildId: 1, + name: 'A', + style: {}, + code: '', + children: [], + isPage: false + }, + { + id: 3, + nextChildId: 1, + name: 'B', + style: {}, + code: '', + children: [], + isPage: false + } +]; + +// renders a tree of the components in tester +describe('Component Tree Render Test', () => { + test('should render full component tree based on state', () => { + render( + + + + ); + // elements that are not separators should appear in the tree + expect(screen.getByText('index')).toBeInTheDocument(); + expect(screen.getByText('A')).toBeInTheDocument(); + expect(screen.getByText('B')).toBeInTheDocument(); + // tree should not include separators + expect(screen.queryByText('separator')).toBe(null); + }); +}); diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index 4a3d8c907..3b1ef8b10 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -1,150 +1,150 @@ -/** - * @jest-environment node - */ - -import app from '../server/server'; -import mockData from '../mockData'; -import { Sessions, Users } from '../server/models/reactypeModels'; -const request = require('supertest'); -const mongoose = require('mongoose'); -const mockNext = jest.fn(); // Mock nextFunction -const MONGO_DB = import.meta.env.MONGO_DB_TEST; -const { user } = mockData; -const PORT = 8080; - -const num = Math.floor(Math.random() * 1000); - -beforeAll(async () => { - await mongoose.connect(MONGO_DB, { - useNewUrlParser: true, - useUnifiedTopology: true - }); -}); - -afterAll(async () => { - const result = await Users.deleteMany({ - _id: { $ne: '64f551e5b28d5292975e08c8' } - }); //clear the users collection after tests are done except for the mockdata user account - const result2 = await Sessions.deleteMany({ - cookieId: { $ne: '64f551e5b28d5292975e08c8' } - }); - console.log( - `${result.deletedCount} and ${result2.deletedCount} documents deleted.` - ); - await mongoose.connection.close(); -}); - -describe('User Authentication tests', () => { - describe('initial connection test', () => { - it('should connect to the server', async () => { - const response = await request(app).get('/test'); - expect(response.status).toBe(200); - expect(response.text).toBe('test request is working'); - }); - }); - describe('/signup', () => { - describe('POST', () => { - //testing new signup - it('responds with status 200 and sessionId on valid new user signup', () => { - return request(app) - .post('/signup') - .set('Content-Type', 'application/json') - .send({ - username: `supertest${num}`, - email: `test${num}@test.com`, - password: `${num}` - }) - .expect(200) - .then((res) => expect(res.body.sessionId).not.toBeNull()); - }); - - it('responds with status 400 and json string on invalid new user signup (Already taken)', () => { - return request(app) - .post('/signup') - .send(user) - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(400) - .then((res) => expect(typeof res.body).toBe('string')); - }); - }); - }); - - describe('/login', () => { - // tests whether existing login information permits user to log in - describe('POST', () => { - it('responds with status 200 and json object on verified user login', () => { - return request(app) - .post('/login') - .set('Accept', 'application/json') - .send(user) - .expect(200) - .expect('Content-Type', /json/) - .then((res) => expect(res.body.sessionId).toEqual(user.userId)); - }); - // if invalid username/password, should respond with status 400 - it('responds with status 400 and json string on invalid user login', () => { - return request(app) - .post('/login') - .send({ username: 'wrongusername', password: 'wrongpassword' }) - .expect(400) - .expect('Content-Type', /json/) - .then((res) => expect(typeof res.body).toBe('string')); - }); - it("returns the message 'No Username Input' when no username is entered", () => { - return request(app) - .post('/login') - .send({ - username: '', - password: 'Reactype123!@#', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"No Username Input"')); - }); - - it("returns the message 'No Username Input' when no username is entered", () => { - return request(app) - .post('/login') - .send({ - username: '', - password: 'Reactype123!@#', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"No Username Input"')); - }); - - it("returns the message 'No Password Input' when no password is entered", () => { - return request(app) - .post('/login') - .send({ - username: 'reactype123', - password: '', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"No Password Input"')); - }); - - it("returns the message 'Invalid Username' when username does not exist", () => { - return request(app) - .post('/login') - .send({ - username: 'l!b', - password: 'test', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"Invalid Username"')); - }); - }); - - it("returns the message 'Incorrect Password' when password does not match", () => { - return request(app) - .post('/login') - .send({ - username: 'test', - password: 'test', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"Incorrect Password"')); - }); - }); -}); +/** + * @jest-environment node + */ + +import app from '../server/server'; +import mockData from '../mockData'; +import { Sessions, Users } from '../server/models/reactypeModels'; +const request = require('supertest'); +const mongoose = require('mongoose'); +const mockNext = jest.fn(); // Mock nextFunction +const MONGO_DB = import.meta.env.MONGO_DB_TEST; +const { user } = mockData; +const PORT = 8080; + +const num = Math.floor(Math.random() * 1000); + +beforeAll(async () => { + await mongoose.connect(MONGO_DB, { + useNewUrlParser: true, + useUnifiedTopology: true + }); +}); + +afterAll(async () => { + const result = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result2 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); + console.log( + `${result.deletedCount} and ${result2.deletedCount} documents deleted.` + ); + await mongoose.connection.close(); +}); + +describe('User Authentication tests', () => { + describe('initial connection test', () => { + it('should connect to the server', async () => { + const response = await request(app).get('/test'); + expect(response.status).toBe(200); + expect(response.text).toBe('test request is working'); + }); + }); + describe('/signup', () => { + describe('POST', () => { + //testing new signup + it('responds with status 200 and sessionId on valid new user signup', () => { + return request(app) + .post('/signup') + .set('Content-Type', 'application/json') + .send({ + username: `supertest${num}`, + email: `test${num}@test.com`, + password: `${num}` + }) + .expect(200) + .then((res) => expect(res.body.sessionId).not.toBeNull()); + }); + + it('responds with status 400 and json string on invalid new user signup (Already taken)', () => { + return request(app) + .post('/signup') + .send(user) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => expect(typeof res.body).toBe('string')); + }); + }); + }); + + describe('/login', () => { + // tests whether existing login information permits user to log in + describe('POST', () => { + it('responds with status 200 and json object on verified user login', () => { + return request(app) + .post('/login') + .set('Accept', 'application/json') + .send(user) + .expect(200) + .expect('Content-Type', /json/) + .then((res) => expect(res.body.sessionId).toEqual(user.userId)); + }); + // if invalid username/password, should respond with status 400 + it('responds with status 400 and json string on invalid user login', () => { + return request(app) + .post('/login') + .send({ username: 'wrongusername', password: 'wrongpassword' }) + .expect(400) + .expect('Content-Type', /json/) + .then((res) => expect(typeof res.body).toBe('string')); + }); + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); + + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); + + it("returns the message 'No Password Input' when no password is entered", () => { + return request(app) + .post('/login') + .send({ + username: 'reactype123', + password: '', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Password Input"')); + }); + + it("returns the message 'Invalid Username' when username does not exist", () => { + return request(app) + .post('/login') + .send({ + username: 'l!b', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Invalid Username"')); + }); + }); + + it("returns the message 'Incorrect Password' when password does not match", () => { + return request(app) + .post('/login') + .send({ + username: 'test', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Incorrect Password"')); + }); + }); +}); diff --git a/amplify/.config/project-config.json b/amplify/.config/project-config.json index 138a6f031..771455955 100644 --- a/amplify/.config/project-config.json +++ b/amplify/.config/project-config.json @@ -1,17 +1,17 @@ -{ - "projectName": "ReacType", - "version": "3.1", - "frontend": "javascript", - "javascript": { - "framework": "none", - "config": { - "SourceDir": "src", - "DistributionDir": "dist", - "BuildCommand": "npm run-script build", - "StartCommand": "npm run-script start" - } - }, - "providers": [ - "awscloudformation" - ] +{ + "projectName": "ReacType", + "version": "3.1", + "frontend": "javascript", + "javascript": { + "framework": "none", + "config": { + "SourceDir": "src", + "DistributionDir": "dist", + "BuildCommand": "npm run-script build", + "StartCommand": "npm run-script start" + } + }, + "providers": [ + "awscloudformation" + ] } \ No newline at end of file diff --git a/amplify/README.md b/amplify/README.md index 7c0a9e285..a2b896041 100644 --- a/amplify/README.md +++ b/amplify/README.md @@ -1,8 +1,8 @@ -# Getting Started with Amplify CLI -This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). - -Helpful resources: -- Amplify documentation: https://docs.amplify.aws -- Amplify CLI documentation: https://docs.amplify.aws/cli -- More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files -- Join Amplify's community: https://amplify.aws/community/ +# Getting Started with Amplify CLI +This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). + +Helpful resources: +- Amplify documentation: https://docs.amplify.aws +- Amplify CLI documentation: https://docs.amplify.aws/cli +- More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files +- Join Amplify's community: https://amplify.aws/community/ diff --git a/amplify/backend/auth/reactype24e8d371/cli-inputs.json b/amplify/backend/auth/reactype24e8d371/cli-inputs.json index 764835fd1..d147a57e8 100644 --- a/amplify/backend/auth/reactype24e8d371/cli-inputs.json +++ b/amplify/backend/auth/reactype24e8d371/cli-inputs.json @@ -1,59 +1,59 @@ -{ - "version": "1", - "cognitoConfig": { - "identityPoolName": "reactype24e8d371_identitypool_24e8d371", - "allowUnauthenticatedIdentities": true, - "resourceNameTruncated": "reacty24e8d371", - "userPoolName": "reactype24e8d371_userpool_24e8d371", - "autoVerifiedAttributes": [ - "email" - ], - "mfaConfiguration": "OFF", - "mfaTypes": [ - "SMS Text Message" - ], - "smsAuthenticationMessage": "Your authentication code is {####}", - "smsVerificationMessage": "Your verification code is {####}", - "emailVerificationSubject": "Your verification code", - "emailVerificationMessage": "Your verification code is {####}", - "defaultPasswordPolicy": false, - "passwordPolicyMinLength": 8, - "passwordPolicyCharacters": [], - "requiredAttributes": [ - "email" - ], - "aliasAttributes": [], - "userpoolClientGenerateSecret": false, - "userpoolClientRefreshTokenValidity": 30, - "userpoolClientWriteAttributes": [ - "email" - ], - "userpoolClientReadAttributes": [ - "email" - ], - "userpoolClientLambdaRole": "reacty24e8d371_userpoolclient_lambda_role", - "userpoolClientSetAttributes": false, - "sharedId": "24e8d371", - "resourceName": "reactype24e8d371", - "authSelections": "identityPoolAndUserPool", - "useDefault": "default", - "userPoolGroupList": [], - "serviceName": "Cognito", - "usernameCaseSensitive": false, - "useEnabledMfas": true, - "authRoleArn": { - "Fn::GetAtt": [ - "AuthRole", - "Arn" - ] - }, - "unauthRoleArn": { - "Fn::GetAtt": [ - "UnauthRole", - "Arn" - ] - }, - "breakCircularDependency": true, - "dependsOn": [] - } +{ + "version": "1", + "cognitoConfig": { + "identityPoolName": "reactype24e8d371_identitypool_24e8d371", + "allowUnauthenticatedIdentities": true, + "resourceNameTruncated": "reacty24e8d371", + "userPoolName": "reactype24e8d371_userpool_24e8d371", + "autoVerifiedAttributes": [ + "email" + ], + "mfaConfiguration": "OFF", + "mfaTypes": [ + "SMS Text Message" + ], + "smsAuthenticationMessage": "Your authentication code is {####}", + "smsVerificationMessage": "Your verification code is {####}", + "emailVerificationSubject": "Your verification code", + "emailVerificationMessage": "Your verification code is {####}", + "defaultPasswordPolicy": false, + "passwordPolicyMinLength": 8, + "passwordPolicyCharacters": [], + "requiredAttributes": [ + "email" + ], + "aliasAttributes": [], + "userpoolClientGenerateSecret": false, + "userpoolClientRefreshTokenValidity": 30, + "userpoolClientWriteAttributes": [ + "email" + ], + "userpoolClientReadAttributes": [ + "email" + ], + "userpoolClientLambdaRole": "reacty24e8d371_userpoolclient_lambda_role", + "userpoolClientSetAttributes": false, + "sharedId": "24e8d371", + "resourceName": "reactype24e8d371", + "authSelections": "identityPoolAndUserPool", + "useDefault": "default", + "userPoolGroupList": [], + "serviceName": "Cognito", + "usernameCaseSensitive": false, + "useEnabledMfas": true, + "authRoleArn": { + "Fn::GetAtt": [ + "AuthRole", + "Arn" + ] + }, + "unauthRoleArn": { + "Fn::GetAtt": [ + "UnauthRole", + "Arn" + ] + }, + "breakCircularDependency": true, + "dependsOn": [] + } } \ No newline at end of file diff --git a/amplify/backend/backend-config.json b/amplify/backend/backend-config.json index 40e757e58..7f7588335 100644 --- a/amplify/backend/backend-config.json +++ b/amplify/backend/backend-config.json @@ -1,35 +1,35 @@ -{ - "auth": { - "reactype24e8d371": { - "customAuth": false, - "dependsOn": [], - "frontendAuthConfig": { - "mfaConfiguration": "OFF", - "mfaTypes": [ - "SMS" - ], - "passwordProtectionSettings": { - "passwordPolicyCharacters": [], - "passwordPolicyMinLength": 8 - }, - "signupAttributes": [ - "EMAIL" - ], - "socialProviders": [], - "usernameAttributes": [], - "verificationMechanisms": [ - "EMAIL" - ] - }, - "providerPlugin": "awscloudformation", - "service": "Cognito" - } - }, - "storage": { - "ReacTypeMktImg": { - "dependsOn": [], - "providerPlugin": "awscloudformation", - "service": "S3" - } - } +{ + "auth": { + "reactype24e8d371": { + "customAuth": false, + "dependsOn": [], + "frontendAuthConfig": { + "mfaConfiguration": "OFF", + "mfaTypes": [ + "SMS" + ], + "passwordProtectionSettings": { + "passwordPolicyCharacters": [], + "passwordPolicyMinLength": 8 + }, + "signupAttributes": [ + "EMAIL" + ], + "socialProviders": [], + "usernameAttributes": [], + "verificationMechanisms": [ + "EMAIL" + ] + }, + "providerPlugin": "awscloudformation", + "service": "Cognito" + } + }, + "storage": { + "ReacTypeMktImg": { + "dependsOn": [], + "providerPlugin": "awscloudformation", + "service": "S3" + } + } } \ No newline at end of file diff --git a/amplify/backend/storage/ReacTypeMktImg/cli-inputs.json b/amplify/backend/storage/ReacTypeMktImg/cli-inputs.json index 3ef215a8c..318329b85 100644 --- a/amplify/backend/storage/ReacTypeMktImg/cli-inputs.json +++ b/amplify/backend/storage/ReacTypeMktImg/cli-inputs.json @@ -1,16 +1,16 @@ -{ - "resourceName": "ReacTypeMktImg", - "policyUUID": "f87a3e6a", - "bucketName": "reactypemktimgs", - "storageAccess": "authAndGuest", - "guestAccess": [ - "READ" - ], - "authAccess": [ - "CREATE_AND_UPDATE", - "READ", - "DELETE" - ], - "triggerFunction": "NONE", - "groupAccess": {} +{ + "resourceName": "ReacTypeMktImg", + "policyUUID": "f87a3e6a", + "bucketName": "reactypemktimgs", + "storageAccess": "authAndGuest", + "guestAccess": [ + "READ" + ], + "authAccess": [ + "CREATE_AND_UPDATE", + "READ", + "DELETE" + ], + "triggerFunction": "NONE", + "groupAccess": {} } \ No newline at end of file diff --git a/amplify/backend/tags.json b/amplify/backend/tags.json index b9321d71b..e66b26da8 100644 --- a/amplify/backend/tags.json +++ b/amplify/backend/tags.json @@ -1,10 +1,10 @@ -[ - { - "Key": "user:Stack", - "Value": "{project-env}" - }, - { - "Key": "user:Application", - "Value": "{project-name}" - } +[ + { + "Key": "user:Stack", + "Value": "{project-env}" + }, + { + "Key": "user:Application", + "Value": "{project-name}" + } ] \ No newline at end of file diff --git a/amplify/backend/types/amplify-dependent-resources-ref.d.ts b/amplify/backend/types/amplify-dependent-resources-ref.d.ts index e6dd1d4f4..869a4e4a6 100644 --- a/amplify/backend/types/amplify-dependent-resources-ref.d.ts +++ b/amplify/backend/types/amplify-dependent-resources-ref.d.ts @@ -1,19 +1,19 @@ -export type AmplifyDependentResourcesAttributes = { - "auth": { - "reactype24e8d371": { - "AppClientID": "string", - "AppClientIDWeb": "string", - "IdentityPoolId": "string", - "IdentityPoolName": "string", - "UserPoolArn": "string", - "UserPoolId": "string", - "UserPoolName": "string" - } - }, - "storage": { - "ReacTypeMktImg": { - "BucketName": "string", - "Region": "string" - } - } +export type AmplifyDependentResourcesAttributes = { + "auth": { + "reactype24e8d371": { + "AppClientID": "string", + "AppClientIDWeb": "string", + "IdentityPoolId": "string", + "IdentityPoolName": "string", + "UserPoolArn": "string", + "UserPoolId": "string", + "UserPoolName": "string" + } + }, + "storage": { + "ReacTypeMktImg": { + "BucketName": "string", + "Region": "string" + } + } } \ No newline at end of file diff --git a/amplify/cli.json b/amplify/cli.json index 1058d7b08..1dfb3291f 100644 --- a/amplify/cli.json +++ b/amplify/cli.json @@ -1,63 +1,63 @@ -{ - "features": { - "graphqltransformer": { - "addmissingownerfields": true, - "improvepluralization": false, - "validatetypenamereservedwords": true, - "useexperimentalpipelinedtransformer": true, - "enableiterativegsiupdates": true, - "secondarykeyasgsi": true, - "skipoverridemutationinputtypes": true, - "transformerversion": 2, - "suppressschemamigrationprompt": true, - "securityenhancementnotification": false, - "showfieldauthnotification": false, - "usesubusernamefordefaultidentityclaim": true, - "usefieldnameforprimarykeyconnectionfield": false, - "enableautoindexquerynames": true, - "respectprimarykeyattributesonconnectionfield": true, - "shoulddeepmergedirectiveconfigdefaults": false, - "populateownerfieldforstaticgroupauth": true - }, - "frontend-ios": { - "enablexcodeintegration": true - }, - "auth": { - "enablecaseinsensitivity": true, - "useinclusiveterminology": true, - "breakcirculardependency": true, - "forcealiasattributes": false, - "useenabledmfas": true - }, - "codegen": { - "useappsyncmodelgenplugin": true, - "usedocsgeneratorplugin": true, - "usetypesgeneratorplugin": true, - "cleangeneratedmodelsdirectory": true, - "retaincasestyle": true, - "addtimestampfields": true, - "handlelistnullabilitytransparently": true, - "emitauthprovider": true, - "generateindexrules": true, - "enabledartnullsafety": true, - "generatemodelsforlazyloadandcustomselectionset": false - }, - "appsync": { - "generategraphqlpermissions": true - }, - "latestregionsupport": { - "pinpoint": 1, - "translate": 1, - "transcribe": 1, - "rekognition": 1, - "textract": 1, - "comprehend": 1 - }, - "project": { - "overrides": true - } - }, - "debug": { - "shareProjectConfig": false - } +{ + "features": { + "graphqltransformer": { + "addmissingownerfields": true, + "improvepluralization": false, + "validatetypenamereservedwords": true, + "useexperimentalpipelinedtransformer": true, + "enableiterativegsiupdates": true, + "secondarykeyasgsi": true, + "skipoverridemutationinputtypes": true, + "transformerversion": 2, + "suppressschemamigrationprompt": true, + "securityenhancementnotification": false, + "showfieldauthnotification": false, + "usesubusernamefordefaultidentityclaim": true, + "usefieldnameforprimarykeyconnectionfield": false, + "enableautoindexquerynames": true, + "respectprimarykeyattributesonconnectionfield": true, + "shoulddeepmergedirectiveconfigdefaults": false, + "populateownerfieldforstaticgroupauth": true + }, + "frontend-ios": { + "enablexcodeintegration": true + }, + "auth": { + "enablecaseinsensitivity": true, + "useinclusiveterminology": true, + "breakcirculardependency": true, + "forcealiasattributes": false, + "useenabledmfas": true + }, + "codegen": { + "useappsyncmodelgenplugin": true, + "usedocsgeneratorplugin": true, + "usetypesgeneratorplugin": true, + "cleangeneratedmodelsdirectory": true, + "retaincasestyle": true, + "addtimestampfields": true, + "handlelistnullabilitytransparently": true, + "emitauthprovider": true, + "generateindexrules": true, + "enabledartnullsafety": true, + "generatemodelsforlazyloadandcustomselectionset": false + }, + "appsync": { + "generategraphqlpermissions": true + }, + "latestregionsupport": { + "pinpoint": 1, + "translate": 1, + "transcribe": 1, + "rekognition": 1, + "textract": 1, + "comprehend": 1 + }, + "project": { + "overrides": true + } + }, + "debug": { + "shareProjectConfig": false + } } \ No newline at end of file diff --git a/amplify/hooks/README.md b/amplify/hooks/README.md index 8fb601eae..4f0609104 100644 --- a/amplify/hooks/README.md +++ b/amplify/hooks/README.md @@ -1,7 +1,7 @@ -# Command Hooks - -Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. - -To get started, add your script files based on the expected naming convention in this directory. - -Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks +# Command Hooks + +Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. + +To get started, add your script files based on the expected naming convention in this directory. + +Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks diff --git a/app/.electron/main.ts b/app/.electron/main.ts index 6bf974c01..f4cf9ef33 100644 --- a/app/.electron/main.ts +++ b/app/.electron/main.ts @@ -1,511 +1,511 @@ -/* -@description: main.js is what controls the lifecycle of the electron application from initialization to termination. -@actions: codes for Github Oauth has been commented out because of lack of functionality. -*/ -require('dotenv').config(); -const { DEV_PORT } = require('../../config.js'); -const path = require('path'); -import { - app, - protocol, - BrowserWindow, - session, - ipcMain, - dialog -} from 'electron'; -// The splash screen is what appears while the app is loading -// const { initSplashScreen, OfficeTemplate } = require('electron-splashscreen'); -import { resolve } from 'app-root-path'; -// to install react dev tool extension -// const { -// default: installExtension, -// REACT_DEVELOPER_TOOLS -// } = require('electron-devtools-installer'); -import debug from 'electron-debug'; -// import custom protocol in protocol.js -import Protocol from './protocol'; -// menu from another file to modularize the code - -import MenuBuilder from './menu'; - -// mode that the app is running in -const isDev = - import.meta.env.NODE_ENV === 'development' || - import.meta.env.NODE_ENV === 'test'; -const port = 8080; -const selfHost = `http://localhost:${port}`; - -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. -let win: BrowserWindow | null; -let menuBuilder: any; - -// function to create a new browser window -// this function will be called when Electron has initialized itself -async function createWindow() { - // install react dev tools if we are in development mode - // this will happen before creating the browser window. it returns a Boolean whether the protocol of scheme 'app://' was successfully registered and a file (index-prod.html) was sent as the response - protocol.registerBufferProtocol(Protocol.scheme, Protocol.requestHandler); - - // Create the browser window. - win = new BrowserWindow({ - // full screen - width: 1920, - height: 1080, - - minWidth: 980, - // window title - title: 'ReacType', - // the browser window will not display initially as it's loading - // once the browser window renders, a function is called below that hides the splash screen and displays the browser window - show: false, - webPreferences: { - zoomFactor: 0.7, - // enable devtools when in development mode - devTools: true, - // crucial security feature - blocks rendering process from having access to node modules - nodeIntegration: true, - // web workers will not have access to node - nodeIntegrationInWorker: false, - // disallow experimental feature to allow node.js support in sub-frames (i-frames/child windows) - nodeIntegrationInSubFrames: true, - // runs electron apis and preload script in a separate JS context - // separate context has access to document/window but has it's own built-ins and is isolate from changes to global environment by located page - // Electron API only available from preload, not loaded page - contextIsolation: true, - // disables remote module. critical for ensuring that rendering process doesn't have access to node functionality - enableRemoteModule: true, - // path of preload script. preload is how the renderer page will have access to electron functionality - preload: path.join(__dirname, 'preload.js'), - nativeWindowOpen: true - } - }); - - // Splash screen that appears while loading - // const hideSplashscreen = initSplashScreen({ - // mainWindow: win, - // icon: resolve('app/src/public/icons/png/64x64.png'), - // url: OfficeTemplate, - // width: 500, - // height: 300, - // brand: 'OS Labs', - // productName: 'ReacType', - // logo: resolve('app/src/public/icons/png/64x64.png'), - // color: '#3BBCAF', - // website: 'www.reactype.io', - // text: 'Initializing ...', - // }); - // Load app - if (isDev) { - // load app from web-dev server - win.loadURL(selfHost); - } else { - // load local file if in production - win.loadURL(`${Protocol.scheme}://rse/index-prod.html`); - } - - // load page once window is loaded - win.once('ready-to-show', () => { - win!.show(); - }); - - // automatically open DevTools when opening the app - // Note: devtools is creating many errors in the logs at the moment but can't figure out how to resolve the issue - if (isDev) { - win.webContents.once('dom-ready', () => { - debug(); - win!.webContents.openDevTools(); - }); - } - - // Emitted when the window is closed. - win.on('closed', () => { - // Dereference the window object, usually you would store windows - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. - app.quit(); - }); - - menuBuilder = MenuBuilder(win!, 'ReacType'); - menuBuilder.buildMenu(); - - // Removed this security feature for now since it's not being used - // https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content - // TODO: is this the same type of sessions that have in react type - // Could potentially remove this session capability - it appears to be more focused on approving requests from 3rd party notifications - const ses = session; - - const partition = 'default'; - ses - .fromPartition(partition) - .setPermissionRequestHandler((webContents, permission, callback) => { - const allowedPermissions: string[] = []; // Full list here: https://developer.chrome.com/extensions/declare_permissions#manifest - - if (allowedPermissions.includes(permission)) { - callback(true); // Approve permission request - } else { - console.error( - `The application tried to request permission for '${permission}'. This permission was not whitelisted and has been blocked.` - ); - - callback(false); // Deny - } - }); - - // https://electronjs.org/docs/tutorial/security#1-only-load-secure-content; - // The below code can only run when a scheme and host are defined, I thought - // we could use this over _all_ urls - ses - .fromPartition(partition) - .webRequest.onBeforeRequest( - { urls: ['http://localhost./*'] }, - (listener) => { - if (listener.url.indexOf('http://') >= 0) { - listener.callback({ - cancel: true - }); - } - } - ); -} - -// Needs to be called before app is ready; -// gives our scheme access to load relative files, -// as well as local storage, cookies, etc. -// https://electronjs.org/docs/api/protocol#protocolregisterschemesasprivilegedcustomschemes -protocol.registerSchemesAsPrivileged([ - { - scheme: Protocol.scheme, - privileges: { - standard: true, - secure: true, - allowServiceWorkers: true, - supportFetchAPI: true - } - } -]); - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.on('ready', createWindow); - -// TRYING ELECTRON-WINDOW-MANAGER When the application is ready - -// Quit when all windows are closed. -app.on('window-all-closed', () => { - win!.webContents.executeJavaScript('window.localStorage.clear();'); - app.quit(); -}); - -app.on('activate', () => { - if (win === null) { - createWindow(); - } -}); - -// https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation -// limits navigation within the app to a whitelisted array -// redirects are a common attack vector - -// after the contents of the webpage are rendered, set up event listeners on the webContents -app.on('web-contents-created', (event, contents) => { - contents.on('will-navigate', (event, navigationUrl) => { - const parsedUrl = new URL(navigationUrl); - const validOrigins = [ - selfHost, - //'https://reactype-1.herokuapp.com', - 'https://reactype-caret.herokuapp.com', - `http://localhost:${DEV_PORT}`, - 'https://reactype.herokuapp.com', - 'https://github.com', - 'https://nextjs.org', - 'https://www.facebook.com', - 'https://developer.mozilla.org', - 'https://www.smashingmagazine.com', - 'https://www.html5rocks.com', - 'null', - 'app://rse/' - ]; - // Log and prevent the app from navigating to a new page if that page's origin is not whitelisted - if (!validOrigins.includes(parsedUrl.origin)) { - console.error( - `The application tried to navigate to the following address: '${parsedUrl}'. This origin is not whitelisted attempt to navigate was blocked.` - ); - // if the requested URL is not in the whitelisted array, then don't navigate there - event.preventDefault(); - } - }); - - contents.on('will-redirect', (event, navigationUrl) => { - const parsedUrl = new URL(navigationUrl); - const validOrigins = [ - selfHost, - //'https://reactype-1.herokuapp.com/', - 'https://reactype-caret.herokuapp.com', - `http://localhost:${DEV_PORT}`, - 'https://reactype.herokuapp.com', - 'https://github.com', - 'https://nextjs.org', - 'https://developer.mozilla.org', - 'https://www.facebook.com', - 'https://www.smashingmagazine.com', - 'https://www.html5rocks.com', - 'app://rse/', - 'null' - ]; - // Log and prevent the app from redirecting to a new page - if ( - !validOrigins.includes(parsedUrl.origin) && - !validOrigins.includes(parsedUrl.href) - ) { - console.error( - `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.` - ); - - event.preventDefault(); - } - }); - - // https://electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation - // The web-view is used to embed guest content in a page - // This event listener deletes web-views if they happen to occur in the app - // https://www.electronjs.org/docs/api/web-contents#event-will-attach-webview - contents.on('will-attach-webview', (event, webPreferences, params) => { - // Strip away preload scripts if unused or verify their location is legitimate - delete webPreferences.preload; - delete webPreferences.preloadURL; - - // Disable Node.js integration - webPreferences.nodeIntegration = false; - }); - - // https://electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows - contents.on('new-window', async (event, navigationUrl) => { - // Log and prevent opening up a new window - const parsedUrl = new URL(navigationUrl); - const validOrigins = [ - selfHost, - //'https://reactype-1.herokuapp.com/', - 'https://reactype-caret.herokuapp.com', - `http://localhost:${DEV_PORT}`, - 'https://reactype.herokuapp.com', - 'https://nextjs.org', - 'https://developer.mozilla.org', - 'https://github.com', - 'https://www.facebook.com', - 'https://www.smashingmagazine.com', - 'https://www.html5rocks.com', - 'null', - 'app://rse/' - ]; - // Log and prevent the app from navigating to a new page if that page's origin is not whitelisted - if (!validOrigins.includes(parsedUrl.origin)) { - console.error( - `The application tried to open a new window at the following address: '${navigationUrl}'. This attempt was blocked.` - ); - // if the requested URL is not in the whitelisted array, then don't navigate there - event.preventDefault(); - } - }); -}); - -// Filter loading any module via remote; -// you shouldn't be using remote at all, though -// https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module -app.on('remote-require', (event, webContents, moduleName) => { - event.preventDefault(); -}); - -// built-ins are modules such as "app" -app.on('remote-get-builtin', (event, webContents, moduleName) => { - event.preventDefault(); -}); - -app.on('remote-get-global', (event, webContents, globalName) => { - event.preventDefault(); -}); - -app.on('remote-get-current-window', (event, webContents) => { - event.preventDefault(); -}); - -app.on('remote-get-current-web-contents', (event, webContents) => { - event.preventDefault(); -}); - -// When a user selects "Export project", a function (chooseAppDir loaded via preload.js) -// is triggered that sends a "choose_app_dir" message to the main process -// when the "choose_app_dir" message is received it triggers this event listener -ipcMain.on('choose_app_dir', (event) => { - // dialog displays the native system's dialogue for selecting files - // once a directory is chosen send a message back to the renderer with the path of the directory - dialog - .showOpenDialog(win!, { - properties: ['openDirectory'], - buttonLabel: 'Export' - }) - .then((directory) => { - if (!directory) return; - event.sender.send('app_dir_selected', directory.filePaths[0]); - }) - .catch((err) => console.log('ERROR on "choose_app_dir" event: ', err)); -}); - -// define serverURL for cookie and auth purposes based on environment -let serverUrl = 'https://reactype-caret.herokuapp.com'; -if (isDev) { - serverUrl = `http://localhost:${DEV_PORT}`; -} - -// // for github oauth login in production, since cookies are not accessible through document.cookie on local filesystem, we need electron to grab the cookie that is set from oauth, this listens for an set cookie event from the renderer process then sends back the cookie -ipcMain.on('set_cookie', (event) => { - session.defaultSession.cookies - .get({ url: serverUrl }) - .then((cookie) => { - // this if statement is necessary or the setInterval on main app will constantly run and will emit this event.reply, causing a memory leak - // checking for a cookie inside array will only emit reply when a cookie exists - if (cookie[0]) { - event.reply('give_cookie', cookie); - } - }) - .catch((error) => { - console.log('Error giving cookies in set_cookie:', error); - }); -}); - -// again for production, document.cookie is not accessible so we need this listener on main to delete the cookie on logout -ipcMain.on('delete_cookie', (event) => { - session.defaultSession.cookies - .remove(serverUrl, 'ssid') - // .then(removed => { - // }) - .catch((err) => console.log('Error deleting cookie:', err)); -}); - -// opens new window for github oauth when button on sign in page is clicked -ipcMain.on('github', (event) => { - const githubURL = isDev - ? `http://localhost:${DEV_PORT}/auth/github` - : `https://reactype-caret.herokuapp.com/auth/github`; - const options = { - client_id: import.meta.env.GITHUB_ID, - client_secret: import.meta.env.GITHUB_SECRET, - scopes: ['user:email', 'notifications'] - }; - // create new browser window object with size, title, security options - const github = new BrowserWindow({ - width: 800, - height: 600, - title: 'Github Oauth', - webPreferences: { - nodeIntegration: true, - nodeIntegrationInWorker: false, - nodeIntegrationInSubFrames: false, - contextIsolation: true, - enableRemoteModule: true, - zoomFactor: 1.0 - } - }); - - github.loadURL(githubURL); - github.show(); - const handleCallback = (url) => { - const raw_code = /code=([^&]\*)/.exec(url) || null; - const code = raw_code && raw_code.length > 1 ? raw_code[1] : null; - const error = /\?error=(.+)\$/.exec(url); - - if (code || error) { - // Close the browser if code found or error - github.destroy(); - } - - // If there is a code, proceed to get token from github - if (code) { - self.requestGithubToken(options, code); - } else if (error) { - alert( - "Oops! Something went wrong and we couldn't" + - 'log you in using Github. Please try again.' - ); - } - }; - - github.webContents.on('will-navigate', (e, url) => handleCallback(url)); - - github.webContents.on('did-finish-load', (e, url, a, b) => { - github.webContents.selectAll(); - }); - - github.webContents.on('did-get-redirect-request', (e, oldUrl, newUrl) => - handleCallback(newUrl) - ); - - // Reset the authWindow on close - github.on('close', () => (authWindow = null), false); - - // if final callback is reached and we get a redirect from server back to our app, close oauth window - github.webContents.on('will-redirect', (e, callbackUrl) => { - const matches = callbackUrl.match(/(?<=\?=).*/); - const ssid = matches ? matches[0] : ''; - callbackUrl = callbackUrl.replace(/\?=.*/, ''); - let redirectUrl = 'app://rse/'; - if (isDev) { - redirectUrl = 'http://localhost:8080/'; - } - - if (callbackUrl === redirectUrl) { - dialog.showMessageBox({ - type: 'info', - title: 'ReacType', - icon: resolve('app/src/public/icons/png/256x256.png'), - message: 'Github Oauth Successful!' - }); - github.close(); - win!.webContents - .executeJavaScript(`window.localStorage.setItem('ssid', '${ssid}')`) - .then((result) => win!.loadURL(`${redirectUrl}`)) - .catch((err) => console.log(err)); - } - }); -}); - -ipcMain.on('tutorial', (event) => { - // create new browser window object with size, title, security options - const tutorial = new BrowserWindow({ - width: 800, - height: 600, - minWidth: 661, - title: 'Tutorial', - webPreferences: { - nodeIntegration: true, - nodeIntegrationInWorker: false, - nodeIntegrationInSubFrames: false, - contextIsolation: true, - enableRemoteModule: true, - zoomFactor: 1.0 - } - }); - // redirects to relevant server endpoint - github.loadURL(`${serverUrl}/github`); - // show window - tutorial.show(); - // if final callback is reached and we get a redirect from server back to our app, close oauth window - github.webContents.on('will-redirect', (e, callbackUrl) => { - let redirectUrl = 'app://rse/'; - if (isDev) { - redirectUrl = 'http://localhost:8080/'; - } - if (callbackUrl === redirectUrl) { - dialog.showMessageBox({ - type: 'info', - title: 'ReacType', - icon: resolve('app/src/public/icons/png/256x256.png'), - message: 'Github Oauth Successful!' - }); - github.close(); - } - }); -}); - -module.exports = dialog; +/* +@description: main.js is what controls the lifecycle of the electron application from initialization to termination. +@actions: codes for Github Oauth has been commented out because of lack of functionality. +*/ +require('dotenv').config(); +const { DEV_PORT } = require('../../config.js'); +const path = require('path'); +import { + app, + protocol, + BrowserWindow, + session, + ipcMain, + dialog +} from 'electron'; +// The splash screen is what appears while the app is loading +// const { initSplashScreen, OfficeTemplate } = require('electron-splashscreen'); +import { resolve } from 'app-root-path'; +// to install react dev tool extension +// const { +// default: installExtension, +// REACT_DEVELOPER_TOOLS +// } = require('electron-devtools-installer'); +import debug from 'electron-debug'; +// import custom protocol in protocol.js +import Protocol from './protocol'; +// menu from another file to modularize the code + +import MenuBuilder from './menu'; + +// mode that the app is running in +const isDev = + import.meta.env.NODE_ENV === 'development' || + import.meta.env.NODE_ENV === 'test'; +const port = 8080; +const selfHost = `http://localhost:${port}`; + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let win: BrowserWindow | null; +let menuBuilder: any; + +// function to create a new browser window +// this function will be called when Electron has initialized itself +async function createWindow() { + // install react dev tools if we are in development mode + // this will happen before creating the browser window. it returns a Boolean whether the protocol of scheme 'app://' was successfully registered and a file (index-prod.html) was sent as the response + protocol.registerBufferProtocol(Protocol.scheme, Protocol.requestHandler); + + // Create the browser window. + win = new BrowserWindow({ + // full screen + width: 1920, + height: 1080, + + minWidth: 980, + // window title + title: 'ReacType', + // the browser window will not display initially as it's loading + // once the browser window renders, a function is called below that hides the splash screen and displays the browser window + show: false, + webPreferences: { + zoomFactor: 0.7, + // enable devtools when in development mode + devTools: true, + // crucial security feature - blocks rendering process from having access to node modules + nodeIntegration: true, + // web workers will not have access to node + nodeIntegrationInWorker: false, + // disallow experimental feature to allow node.js support in sub-frames (i-frames/child windows) + nodeIntegrationInSubFrames: true, + // runs electron apis and preload script in a separate JS context + // separate context has access to document/window but has it's own built-ins and is isolate from changes to global environment by located page + // Electron API only available from preload, not loaded page + contextIsolation: true, + // disables remote module. critical for ensuring that rendering process doesn't have access to node functionality + enableRemoteModule: true, + // path of preload script. preload is how the renderer page will have access to electron functionality + preload: path.join(__dirname, 'preload.js'), + nativeWindowOpen: true + } + }); + + // Splash screen that appears while loading + // const hideSplashscreen = initSplashScreen({ + // mainWindow: win, + // icon: resolve('app/src/public/icons/png/64x64.png'), + // url: OfficeTemplate, + // width: 500, + // height: 300, + // brand: 'OS Labs', + // productName: 'ReacType', + // logo: resolve('app/src/public/icons/png/64x64.png'), + // color: '#3BBCAF', + // website: 'www.reactype.io', + // text: 'Initializing ...', + // }); + // Load app + if (isDev) { + // load app from web-dev server + win.loadURL(selfHost); + } else { + // load local file if in production + win.loadURL(`${Protocol.scheme}://rse/index-prod.html`); + } + + // load page once window is loaded + win.once('ready-to-show', () => { + win!.show(); + }); + + // automatically open DevTools when opening the app + // Note: devtools is creating many errors in the logs at the moment but can't figure out how to resolve the issue + if (isDev) { + win.webContents.once('dom-ready', () => { + debug(); + win!.webContents.openDevTools(); + }); + } + + // Emitted when the window is closed. + win.on('closed', () => { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + app.quit(); + }); + + menuBuilder = MenuBuilder(win!, 'ReacType'); + menuBuilder.buildMenu(); + + // Removed this security feature for now since it's not being used + // https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content + // TODO: is this the same type of sessions that have in react type + // Could potentially remove this session capability - it appears to be more focused on approving requests from 3rd party notifications + const ses = session; + + const partition = 'default'; + ses + .fromPartition(partition) + .setPermissionRequestHandler((webContents, permission, callback) => { + const allowedPermissions: string[] = []; // Full list here: https://developer.chrome.com/extensions/declare_permissions#manifest + + if (allowedPermissions.includes(permission)) { + callback(true); // Approve permission request + } else { + console.error( + `The application tried to request permission for '${permission}'. This permission was not whitelisted and has been blocked.` + ); + + callback(false); // Deny + } + }); + + // https://electronjs.org/docs/tutorial/security#1-only-load-secure-content; + // The below code can only run when a scheme and host are defined, I thought + // we could use this over _all_ urls + ses + .fromPartition(partition) + .webRequest.onBeforeRequest( + { urls: ['http://localhost./*'] }, + (listener) => { + if (listener.url.indexOf('http://') >= 0) { + listener.callback({ + cancel: true + }); + } + } + ); +} + +// Needs to be called before app is ready; +// gives our scheme access to load relative files, +// as well as local storage, cookies, etc. +// https://electronjs.org/docs/api/protocol#protocolregisterschemesasprivilegedcustomschemes +protocol.registerSchemesAsPrivileged([ + { + scheme: Protocol.scheme, + privileges: { + standard: true, + secure: true, + allowServiceWorkers: true, + supportFetchAPI: true + } + } +]); + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow); + +// TRYING ELECTRON-WINDOW-MANAGER When the application is ready + +// Quit when all windows are closed. +app.on('window-all-closed', () => { + win!.webContents.executeJavaScript('window.localStorage.clear();'); + app.quit(); +}); + +app.on('activate', () => { + if (win === null) { + createWindow(); + } +}); + +// https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation +// limits navigation within the app to a whitelisted array +// redirects are a common attack vector + +// after the contents of the webpage are rendered, set up event listeners on the webContents +app.on('web-contents-created', (event, contents) => { + contents.on('will-navigate', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + const validOrigins = [ + selfHost, + //'https://reactype-1.herokuapp.com', + 'https://reactype-caret.herokuapp.com', + `http://localhost:${DEV_PORT}`, + 'https://reactype.herokuapp.com', + 'https://github.com', + 'https://nextjs.org', + 'https://www.facebook.com', + 'https://developer.mozilla.org', + 'https://www.smashingmagazine.com', + 'https://www.html5rocks.com', + 'null', + 'app://rse/' + ]; + // Log and prevent the app from navigating to a new page if that page's origin is not whitelisted + if (!validOrigins.includes(parsedUrl.origin)) { + console.error( + `The application tried to navigate to the following address: '${parsedUrl}'. This origin is not whitelisted attempt to navigate was blocked.` + ); + // if the requested URL is not in the whitelisted array, then don't navigate there + event.preventDefault(); + } + }); + + contents.on('will-redirect', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + const validOrigins = [ + selfHost, + //'https://reactype-1.herokuapp.com/', + 'https://reactype-caret.herokuapp.com', + `http://localhost:${DEV_PORT}`, + 'https://reactype.herokuapp.com', + 'https://github.com', + 'https://nextjs.org', + 'https://developer.mozilla.org', + 'https://www.facebook.com', + 'https://www.smashingmagazine.com', + 'https://www.html5rocks.com', + 'app://rse/', + 'null' + ]; + // Log and prevent the app from redirecting to a new page + if ( + !validOrigins.includes(parsedUrl.origin) && + !validOrigins.includes(parsedUrl.href) + ) { + console.error( + `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.` + ); + + event.preventDefault(); + } + }); + + // https://electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation + // The web-view is used to embed guest content in a page + // This event listener deletes web-views if they happen to occur in the app + // https://www.electronjs.org/docs/api/web-contents#event-will-attach-webview + contents.on('will-attach-webview', (event, webPreferences, params) => { + // Strip away preload scripts if unused or verify their location is legitimate + delete webPreferences.preload; + delete webPreferences.preloadURL; + + // Disable Node.js integration + webPreferences.nodeIntegration = false; + }); + + // https://electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows + contents.on('new-window', async (event, navigationUrl) => { + // Log and prevent opening up a new window + const parsedUrl = new URL(navigationUrl); + const validOrigins = [ + selfHost, + //'https://reactype-1.herokuapp.com/', + 'https://reactype-caret.herokuapp.com', + `http://localhost:${DEV_PORT}`, + 'https://reactype.herokuapp.com', + 'https://nextjs.org', + 'https://developer.mozilla.org', + 'https://github.com', + 'https://www.facebook.com', + 'https://www.smashingmagazine.com', + 'https://www.html5rocks.com', + 'null', + 'app://rse/' + ]; + // Log and prevent the app from navigating to a new page if that page's origin is not whitelisted + if (!validOrigins.includes(parsedUrl.origin)) { + console.error( + `The application tried to open a new window at the following address: '${navigationUrl}'. This attempt was blocked.` + ); + // if the requested URL is not in the whitelisted array, then don't navigate there + event.preventDefault(); + } + }); +}); + +// Filter loading any module via remote; +// you shouldn't be using remote at all, though +// https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module +app.on('remote-require', (event, webContents, moduleName) => { + event.preventDefault(); +}); + +// built-ins are modules such as "app" +app.on('remote-get-builtin', (event, webContents, moduleName) => { + event.preventDefault(); +}); + +app.on('remote-get-global', (event, webContents, globalName) => { + event.preventDefault(); +}); + +app.on('remote-get-current-window', (event, webContents) => { + event.preventDefault(); +}); + +app.on('remote-get-current-web-contents', (event, webContents) => { + event.preventDefault(); +}); + +// When a user selects "Export project", a function (chooseAppDir loaded via preload.js) +// is triggered that sends a "choose_app_dir" message to the main process +// when the "choose_app_dir" message is received it triggers this event listener +ipcMain.on('choose_app_dir', (event) => { + // dialog displays the native system's dialogue for selecting files + // once a directory is chosen send a message back to the renderer with the path of the directory + dialog + .showOpenDialog(win!, { + properties: ['openDirectory'], + buttonLabel: 'Export' + }) + .then((directory) => { + if (!directory) return; + event.sender.send('app_dir_selected', directory.filePaths[0]); + }) + .catch((err) => console.log('ERROR on "choose_app_dir" event: ', err)); +}); + +// define serverURL for cookie and auth purposes based on environment +let serverUrl = 'https://reactype-caret.herokuapp.com'; +if (isDev) { + serverUrl = `http://localhost:${DEV_PORT}`; +} + +// // for github oauth login in production, since cookies are not accessible through document.cookie on local filesystem, we need electron to grab the cookie that is set from oauth, this listens for an set cookie event from the renderer process then sends back the cookie +ipcMain.on('set_cookie', (event) => { + session.defaultSession.cookies + .get({ url: serverUrl }) + .then((cookie) => { + // this if statement is necessary or the setInterval on main app will constantly run and will emit this event.reply, causing a memory leak + // checking for a cookie inside array will only emit reply when a cookie exists + if (cookie[0]) { + event.reply('give_cookie', cookie); + } + }) + .catch((error) => { + console.log('Error giving cookies in set_cookie:', error); + }); +}); + +// again for production, document.cookie is not accessible so we need this listener on main to delete the cookie on logout +ipcMain.on('delete_cookie', (event) => { + session.defaultSession.cookies + .remove(serverUrl, 'ssid') + // .then(removed => { + // }) + .catch((err) => console.log('Error deleting cookie:', err)); +}); + +// opens new window for github oauth when button on sign in page is clicked +ipcMain.on('github', (event) => { + const githubURL = isDev + ? `http://localhost:${DEV_PORT}/auth/github` + : `https://reactype-caret.herokuapp.com/auth/github`; + const options = { + client_id: import.meta.env.GITHUB_ID, + client_secret: import.meta.env.GITHUB_SECRET, + scopes: ['user:email', 'notifications'] + }; + // create new browser window object with size, title, security options + const github = new BrowserWindow({ + width: 800, + height: 600, + title: 'Github Oauth', + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: false, + nodeIntegrationInSubFrames: false, + contextIsolation: true, + enableRemoteModule: true, + zoomFactor: 1.0 + } + }); + + github.loadURL(githubURL); + github.show(); + const handleCallback = (url) => { + const raw_code = /code=([^&]\*)/.exec(url) || null; + const code = raw_code && raw_code.length > 1 ? raw_code[1] : null; + const error = /\?error=(.+)\$/.exec(url); + + if (code || error) { + // Close the browser if code found or error + github.destroy(); + } + + // If there is a code, proceed to get token from github + if (code) { + self.requestGithubToken(options, code); + } else if (error) { + alert( + "Oops! Something went wrong and we couldn't" + + 'log you in using Github. Please try again.' + ); + } + }; + + github.webContents.on('will-navigate', (e, url) => handleCallback(url)); + + github.webContents.on('did-finish-load', (e, url, a, b) => { + github.webContents.selectAll(); + }); + + github.webContents.on('did-get-redirect-request', (e, oldUrl, newUrl) => + handleCallback(newUrl) + ); + + // Reset the authWindow on close + github.on('close', () => (authWindow = null), false); + + // if final callback is reached and we get a redirect from server back to our app, close oauth window + github.webContents.on('will-redirect', (e, callbackUrl) => { + const matches = callbackUrl.match(/(?<=\?=).*/); + const ssid = matches ? matches[0] : ''; + callbackUrl = callbackUrl.replace(/\?=.*/, ''); + let redirectUrl = 'app://rse/'; + if (isDev) { + redirectUrl = 'http://localhost:8080/'; + } + + if (callbackUrl === redirectUrl) { + dialog.showMessageBox({ + type: 'info', + title: 'ReacType', + icon: resolve('app/src/public/icons/png/256x256.png'), + message: 'Github Oauth Successful!' + }); + github.close(); + win!.webContents + .executeJavaScript(`window.localStorage.setItem('ssid', '${ssid}')`) + .then((result) => win!.loadURL(`${redirectUrl}`)) + .catch((err) => console.log(err)); + } + }); +}); + +ipcMain.on('tutorial', (event) => { + // create new browser window object with size, title, security options + const tutorial = new BrowserWindow({ + width: 800, + height: 600, + minWidth: 661, + title: 'Tutorial', + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: false, + nodeIntegrationInSubFrames: false, + contextIsolation: true, + enableRemoteModule: true, + zoomFactor: 1.0 + } + }); + // redirects to relevant server endpoint + github.loadURL(`${serverUrl}/github`); + // show window + tutorial.show(); + // if final callback is reached and we get a redirect from server back to our app, close oauth window + github.webContents.on('will-redirect', (e, callbackUrl) => { + let redirectUrl = 'app://rse/'; + if (isDev) { + redirectUrl = 'http://localhost:8080/'; + } + if (callbackUrl === redirectUrl) { + dialog.showMessageBox({ + type: 'info', + title: 'ReacType', + icon: resolve('app/src/public/icons/png/256x256.png'), + message: 'Github Oauth Successful!' + }); + github.close(); + } + }); +}); + +module.exports = dialog; diff --git a/app/.electron/menu.ts b/app/.electron/menu.ts index c0814ebe2..abaacf72f 100644 --- a/app/.electron/menu.ts +++ b/app/.electron/menu.ts @@ -1,248 +1,248 @@ -import { Menu, BrowserWindow, Shell } from 'electron'; -const isMac = process.platform === 'darwin'; -import Protocol from './protocol'; -/* -DESCRIPTION: This file generates an array containing a menu based on the operating system the user is running. -menuBuilder: The entire file is encompassed in menuBuilder. Ultimately, menuBuilder returns a function called - buildMenu that uses defaultTemplate to construct a menu at the top of the application (as invoked in main.js) - - Standard menu roles (e.g., undo, redo, quit, paste, etc.) come from Electron API and need not be separately coded - -openTutorial: opens browser window containing tutorial on how to use the app - -Creates a browser window - -Tutorial is invoked within the "Help" menu - -defaultTemplate: returns an array of submenus (each an array) - -First, checks whether user is on a Mac (node returns 'darwin' for process.platform) - -Then generates a dropdown menu at the top of the screen (e.g., "File") accordingly - -The Mac check is necessary primarily for the first menu column, which is the name of the app - -If user is not on a Mac, alternative menus are generated - -Each menu: - -"label" is the field at the top of each menu (e.g., "File", "Edit", "View", etc.) - -"role" is a subitem within each menu (e.g., under "File," "Quit") - -"type: separator" creates a horizontal line in a menu (e.g., under "Redo" in the "Edit" menu) -*/ - -// Create a template for a menu and create menu using that template -var MenuBuilder = function (mainWindow, appName) { - // https://electronjs.org/docs/api/menu#main-process - // "roles" are predefined by Electron and used for standard actions - // https://www.electronjs.org/docs/api/menu-item - // you can also create custom menu items with their own "on click" functionality if you need to - // different roles are available between mac and windows - - function openTutorial(): void { - const tutorial = new BrowserWindow({ - width: 1180, - height: 900, - minWidth: 665, - title: 'Tutorial', - webPreferences: { - nodeIntegration: true, - nodeIntegrationInWorker: false, - nodeIntegrationInSubFrames: false, - contextIsolation: true, - enableRemoteModule: true, - zoomFactor: 1.0, - devTools: false - } - }); - if (import.meta.env.NODE_ENV === 'development') { - tutorial.loadURL(`http://localhost:8080/#/tutorial`); - } else { - tutorial.loadURL(`${Protocol.scheme}://rse/index-prod.html#/tutorial`); - } - tutorial.show(); - } - - const defaultTemplate= (): Electron.MenuItemConstructorOptions[] => [ - ...(isMac - ? [ - { - // on Mac, the first menu item name should be the name of the app - label: appName, - submenu: [ - {role: 'about'}, - {type: 'separator'}, - {role: 'services'}, - {type: 'separator'}, - {role: 'hide'}, - {role: 'hideothers'}, - {role: 'unhide'}, - {type: 'separator'}, - {role: 'quit'} - ] as Electron.MenuItemConstructorOptions[], - } , - ] - : []),Electron.MenuItemConstructorOptions[], - { - label: 'File', - submenu: [ - isMac - ? { - role: 'close' - } - : { - role: 'quit' - } - ] - }, - { - label: 'Edit', - submenu: [ - { - role: 'undo' - }, - { - role: 'redo' - }, - { - type: 'separator' - }, - { - role: 'cut' - }, - { - role: 'copy' - }, - { - role: 'paste' - }, - ...(isMac - ? [ - { - role: 'pasteAndMatchStyle' - }, - { - role: 'delete' - }, - { - role: 'selectAll' - }, - { - type: 'separator' - }, - { - label: 'Speech', - submenu: [ - { - role: 'startspeaking' - }, - { - role: 'stopspeaking' - } - ] - } - ] - : [ - { - role: 'delete' - }, - { - type: 'separator' - }, - { - role: 'selectAll' - } - ]) - ] - }, - { - label: 'View', - submenu: [ - { - role: 'reload' - }, - { - role: 'forcereload' - }, - { - role: 'toggledevtools' - }, - { - type: 'separator' - }, - { - role: 'resetzoom' - }, - { - role: 'zoomin' - }, - { - role: 'zoomout' - }, - { - type: 'separator' - }, - { - role: 'togglefullscreen' - } - ] - }, - - { - label: 'Window', - submenu: [ - { - role: 'minimize' - }, - { - role: 'zoom' - }, - ...(isMac - ? [ - { - type: 'separator' - }, - { - role: 'front' - }, - { - type: 'separator' - }, - { - role: 'window' - } - ] - : [ - { - role: 'close' - } - ]) - ] - }, - { - role: 'help', - submenu: [ - { - label: 'Learn More', - click: async () => { - const { shell } = require('electron'); - await shell.openExternal( - 'https://github.com/open-source-labs/ReacType' - ); - } - }, - { - label: 'Tutorial', - click: () => openTutorial() - } - ] - } - ]; - - return template; - } - - // constructs menu from default template - return { - buildMenu: function () { - const menu = Menu.buildFromTemplate(defaultTemplate()); - Menu.setApplicationMenu(menu); - - return menu; - } - }; -}; - -export { MenuBuilder }; +import { Menu, BrowserWindow, Shell } from 'electron'; +const isMac = process.platform === 'darwin'; +import Protocol from './protocol'; +/* +DESCRIPTION: This file generates an array containing a menu based on the operating system the user is running. +menuBuilder: The entire file is encompassed in menuBuilder. Ultimately, menuBuilder returns a function called + buildMenu that uses defaultTemplate to construct a menu at the top of the application (as invoked in main.js) + + Standard menu roles (e.g., undo, redo, quit, paste, etc.) come from Electron API and need not be separately coded + +openTutorial: opens browser window containing tutorial on how to use the app + -Creates a browser window + -Tutorial is invoked within the "Help" menu + +defaultTemplate: returns an array of submenus (each an array) + -First, checks whether user is on a Mac (node returns 'darwin' for process.platform) + -Then generates a dropdown menu at the top of the screen (e.g., "File") accordingly + -The Mac check is necessary primarily for the first menu column, which is the name of the app + -If user is not on a Mac, alternative menus are generated + -Each menu: + -"label" is the field at the top of each menu (e.g., "File", "Edit", "View", etc.) + -"role" is a subitem within each menu (e.g., under "File," "Quit") + -"type: separator" creates a horizontal line in a menu (e.g., under "Redo" in the "Edit" menu) +*/ + +// Create a template for a menu and create menu using that template +var MenuBuilder = function (mainWindow, appName) { + // https://electronjs.org/docs/api/menu#main-process + // "roles" are predefined by Electron and used for standard actions + // https://www.electronjs.org/docs/api/menu-item + // you can also create custom menu items with their own "on click" functionality if you need to + // different roles are available between mac and windows + + function openTutorial(): void { + const tutorial = new BrowserWindow({ + width: 1180, + height: 900, + minWidth: 665, + title: 'Tutorial', + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: false, + nodeIntegrationInSubFrames: false, + contextIsolation: true, + enableRemoteModule: true, + zoomFactor: 1.0, + devTools: false + } + }); + if (import.meta.env.NODE_ENV === 'development') { + tutorial.loadURL(`http://localhost:8080/#/tutorial`); + } else { + tutorial.loadURL(`${Protocol.scheme}://rse/index-prod.html#/tutorial`); + } + tutorial.show(); + } + + const defaultTemplate= (): Electron.MenuItemConstructorOptions[] => [ + ...(isMac + ? [ + { + // on Mac, the first menu item name should be the name of the app + label: appName, + submenu: [ + {role: 'about'}, + {type: 'separator'}, + {role: 'services'}, + {type: 'separator'}, + {role: 'hide'}, + {role: 'hideothers'}, + {role: 'unhide'}, + {type: 'separator'}, + {role: 'quit'} + ] as Electron.MenuItemConstructorOptions[], + } , + ] + : []),Electron.MenuItemConstructorOptions[], + { + label: 'File', + submenu: [ + isMac + ? { + role: 'close' + } + : { + role: 'quit' + } + ] + }, + { + label: 'Edit', + submenu: [ + { + role: 'undo' + }, + { + role: 'redo' + }, + { + type: 'separator' + }, + { + role: 'cut' + }, + { + role: 'copy' + }, + { + role: 'paste' + }, + ...(isMac + ? [ + { + role: 'pasteAndMatchStyle' + }, + { + role: 'delete' + }, + { + role: 'selectAll' + }, + { + type: 'separator' + }, + { + label: 'Speech', + submenu: [ + { + role: 'startspeaking' + }, + { + role: 'stopspeaking' + } + ] + } + ] + : [ + { + role: 'delete' + }, + { + type: 'separator' + }, + { + role: 'selectAll' + } + ]) + ] + }, + { + label: 'View', + submenu: [ + { + role: 'reload' + }, + { + role: 'forcereload' + }, + { + role: 'toggledevtools' + }, + { + type: 'separator' + }, + { + role: 'resetzoom' + }, + { + role: 'zoomin' + }, + { + role: 'zoomout' + }, + { + type: 'separator' + }, + { + role: 'togglefullscreen' + } + ] + }, + + { + label: 'Window', + submenu: [ + { + role: 'minimize' + }, + { + role: 'zoom' + }, + ...(isMac + ? [ + { + type: 'separator' + }, + { + role: 'front' + }, + { + type: 'separator' + }, + { + role: 'window' + } + ] + : [ + { + role: 'close' + } + ]) + ] + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click: async () => { + const { shell } = require('electron'); + await shell.openExternal( + 'https://github.com/open-source-labs/ReacType' + ); + } + }, + { + label: 'Tutorial', + click: () => openTutorial() + } + ] + } + ]; + + return template; + } + + // constructs menu from default template + return { + buildMenu: function () { + const menu = Menu.buildFromTemplate(defaultTemplate()); + Menu.setApplicationMenu(menu); + + return menu; + } + }; +}; + +export { MenuBuilder }; diff --git a/app/.electron/preload.ts b/app/.electron/preload.ts index 0b10eb72c..5fbcfec69 100644 --- a/app/.electron/preload.ts +++ b/app/.electron/preload.ts @@ -1,54 +1,54 @@ -// contextBridge is what allows context to be translated between the main process and the render process -import { contextBridge } from 'electron'; -import { existsSync, writeFileSync, mkdirSync, writeFile } from 'fs'; -import formatCode from './preloadFunctions/format'; -import { - chooseAppDir, - addAppDirChosenListener, - removeAllAppDirChosenListeners -} from './preloadFunctions/chooseAppDir'; -import { - setCookie, - getCookie, - delCookie, - github, - tutorial -} from './preloadFunctions/cookies'; - -/* -DESCRIPTION: This file appears to limit the node methods the Electron app can access. - -Per the docs: - -Main World is the JavaScript context in which the renderer code runs (that is, the page) - -Isolated World is where preload scripts run - -contextBridge is a module that safely exposes APIs from the isolated context in which preload scripts run - to the context in which the website or application runs (i.e., from Isolated World to Main World) - -We likely should not change this file unless we determine additional methods are necessary -or some methods are not used. - -*/ - -// Expose protected methods that allow the renderer process to use select node methods -// without exposing all node functionality. This is a critical security feature -// 'mainWorld" is the context that the main renderer runs in -// with contextIsolation on (see webpreferences on main.js), this preload script runs isolated -// "api" is the key that injects the api into the window -// to access these keys in the renderer process, we'll do window.api -// the api object (second arg) can contain functions, strings, bools, numbers, arrays, obects in value -// data primitives sent on the bridge are immutable and changes in one context won't carry over to another context -contextBridge.exposeInMainWorld('api', { - formatCode, - chooseAppDir, - addAppDirChosenListener, - removeAllAppDirChosenListeners, - existsSync, - writeFileSync, - mkdirSync, - writeFile, - setCookie, - getCookie, - delCookie, - github, - tutorial -}); +// contextBridge is what allows context to be translated between the main process and the render process +import { contextBridge } from 'electron'; +import { existsSync, writeFileSync, mkdirSync, writeFile } from 'fs'; +import formatCode from './preloadFunctions/format'; +import { + chooseAppDir, + addAppDirChosenListener, + removeAllAppDirChosenListeners +} from './preloadFunctions/chooseAppDir'; +import { + setCookie, + getCookie, + delCookie, + github, + tutorial +} from './preloadFunctions/cookies'; + +/* +DESCRIPTION: This file appears to limit the node methods the Electron app can access. + +Per the docs: + -Main World is the JavaScript context in which the renderer code runs (that is, the page) + -Isolated World is where preload scripts run + -contextBridge is a module that safely exposes APIs from the isolated context in which preload scripts run + to the context in which the website or application runs (i.e., from Isolated World to Main World) + +We likely should not change this file unless we determine additional methods are necessary +or some methods are not used. + +*/ + +// Expose protected methods that allow the renderer process to use select node methods +// without exposing all node functionality. This is a critical security feature +// 'mainWorld" is the context that the main renderer runs in +// with contextIsolation on (see webpreferences on main.js), this preload script runs isolated +// "api" is the key that injects the api into the window +// to access these keys in the renderer process, we'll do window.api +// the api object (second arg) can contain functions, strings, bools, numbers, arrays, obects in value +// data primitives sent on the bridge are immutable and changes in one context won't carry over to another context +contextBridge.exposeInMainWorld('api', { + formatCode, + chooseAppDir, + addAppDirChosenListener, + removeAllAppDirChosenListeners, + existsSync, + writeFileSync, + mkdirSync, + writeFile, + setCookie, + getCookie, + delCookie, + github, + tutorial +}); diff --git a/app/.electron/preloadFunctions/chooseAppDir.ts b/app/.electron/preloadFunctions/chooseAppDir.ts index 969b17f56..96ec31b3e 100644 --- a/app/.electron/preloadFunctions/chooseAppDir.ts +++ b/app/.electron/preloadFunctions/chooseAppDir.ts @@ -1,31 +1,31 @@ -import { ipcRenderer, IpcRendererEvent } from 'electron'; -import { type } from 'os'; - -type AppDirSelectedCallback = (path: string) => void; - -const chooseAppDir = (): void => { - ipcRenderer.send('choose_app_dir'); -}; - -// once an app directory is chosen, the main process will send an "app_dir_selected" event -// when this event occurs, exucte the callback passed in by the user -const addAppDirChosenListener = (callback: AppDirSelectedCallback): void => { - ipcRenderer.on( - 'app_dir_selected', - (event: IpcRendererEvent, path: string) => { - callback(path); - } - ); -}; - -// removes all listeners for the app_dir_selected event -// this is important because otherwise listeners will pile up and events will trigger multiple events -const removeAllAppDirChosenListeners = (): void => { - ipcRenderer.removeAllListeners('app_dir_selected'); -}; - -export { - chooseAppDir, - addAppDirChosenListener, - removeAllAppDirChosenListeners -}; +import { ipcRenderer, IpcRendererEvent } from 'electron'; +import { type } from 'os'; + +type AppDirSelectedCallback = (path: string) => void; + +const chooseAppDir = (): void => { + ipcRenderer.send('choose_app_dir'); +}; + +// once an app directory is chosen, the main process will send an "app_dir_selected" event +// when this event occurs, exucte the callback passed in by the user +const addAppDirChosenListener = (callback: AppDirSelectedCallback): void => { + ipcRenderer.on( + 'app_dir_selected', + (event: IpcRendererEvent, path: string) => { + callback(path); + } + ); +}; + +// removes all listeners for the app_dir_selected event +// this is important because otherwise listeners will pile up and events will trigger multiple events +const removeAllAppDirChosenListeners = (): void => { + ipcRenderer.removeAllListeners('app_dir_selected'); +}; + +export { + chooseAppDir, + addAppDirChosenListener, + removeAllAppDirChosenListeners +}; diff --git a/app/.electron/preloadFunctions/cookies.ts b/app/.electron/preloadFunctions/cookies.ts index f5506f83e..da3007610 100644 --- a/app/.electron/preloadFunctions/cookies.ts +++ b/app/.electron/preloadFunctions/cookies.ts @@ -1,27 +1,27 @@ -import { ipcRenderer, IpcRendererEvent } from 'electron'; - -type GetCookieCallback = (cookie: string) => void; - -const setCookie = (): void => { - ipcRenderer.send('set_cookie'); -}; - -const getCookie = (callback: GetCookieCallback): void => { - ipcRenderer.on('give_cookie', (event: IpcRendererEvent, cookie: string) => { - callback(cookie); - }); -}; - -const delCookie = (): void => { - ipcRenderer.send('delete_cookie'); -}; - -const github = (): void => { - ipcRenderer.send('github'); -}; - -const tutorial = (): void => { - ipcRenderer.send('tutorial'); -}; - -export { setCookie, getCookie, delCookie, github, tutorial }; +import { ipcRenderer, IpcRendererEvent } from 'electron'; + +type GetCookieCallback = (cookie: string) => void; + +const setCookie = (): void => { + ipcRenderer.send('set_cookie'); +}; + +const getCookie = (callback: GetCookieCallback): void => { + ipcRenderer.on('give_cookie', (event: IpcRendererEvent, cookie: string) => { + callback(cookie); + }); +}; + +const delCookie = (): void => { + ipcRenderer.send('delete_cookie'); +}; + +const github = (): void => { + ipcRenderer.send('github'); +}; + +const tutorial = (): void => { + ipcRenderer.send('tutorial'); +}; + +export { setCookie, getCookie, delCookie, github, tutorial }; diff --git a/app/.electron/preloadFunctions/format.ts b/app/.electron/preloadFunctions/format.ts index c1ed8e506..9575cc9e6 100644 --- a/app/.electron/preloadFunctions/format.ts +++ b/app/.electron/preloadFunctions/format.ts @@ -1,16 +1,16 @@ -import { format } from 'prettier'; - -// format code using prettier -// this format function is used in the render process to format the code in the code preview -// the format function is defined in the main process because it needs to access node functionality ('fs') -const formatCode = (code: string): string => { - return format(code, { - singleQuote: true, - trailingComma: 'es5', - bracketSpacing: true, - jsxBracketSameLine: true, - parser: 'typescript' - }); -}; - -export default formatCode; +import { format } from 'prettier'; + +// format code using prettier +// this format function is used in the render process to format the code in the code preview +// the format function is defined in the main process because it needs to access node functionality ('fs') +const formatCode = (code: string): string => { + return format(code, { + singleQuote: true, + trailingComma: 'es5', + bracketSpacing: true, + jsxBracketSameLine: true, + parser: 'typescript' + }); +}; + +export default formatCode; diff --git a/app/.electron/protocol.ts b/app/.electron/protocol.ts index a89f0c50e..3f36cf063 100644 --- a/app/.electron/protocol.ts +++ b/app/.electron/protocol.ts @@ -1,76 +1,76 @@ -/* - @desc: register a custom protocol and specify file that will be served on request to the origin '/'. our app will be served from 'app://...' instead of the default 'file://...' - @exports: scheme, requestHandler - @usage: is used in main.js - */ - -const { stringify } = require('querystring'); - -import * as fs from 'fs'; -import * as path from 'path'; - -const DIST_PATH = path.join(__dirname, '../../app/dist'); - -const scheme = 'app'; // it will serve resources like app://..... instead of default file://... - -const mimeTypes: Record = { - '.js': 'text/javascript', - '.mjs': 'text/javascript', - '.html': 'text/html', - '.htm': 'text/html', - '.json': 'application/json', - '.css': 'text/css', - '.svg': 'application/svg+xml', - '.ico': 'image/vnd.microsoft.icon', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.map': 'text/plain' -}; - -function charset(mimeType: string): string | null { - return ['.html', '.htm', '.js', '.mjs'].some((m) => m === mimeType) - ? 'utf-8' - : null; -} -// return the file type -function mime(filename: string): string | null { - const type = mimeTypes[path.extname(`${filename || ''}`).toLowerCase()]; - return type || null; -} - -/* requestHandler - servers index-prod.html when we access the root endpoint '/' - read the file above and pass on an object includes mimeType, charset, and exisiting data read from the file -*/ -function requestHandler( - req: Electron.ProtocolRequest, - next: (response: Electron.ProtocolResponse) => void -): void { - // The URL() constructor returns a newly created URL object representing the URL defined by the parameters. - const reqUrl = new URL(req.url); - // path.normalize resolves '..' and '.' segments in sequential path segments - // url.pathname: an initial '/' followed by the path of the URL not including the query string or fragment (or the empty string if there is no path). - let reqPath = path.normalize(reqUrl.pathname); - - // when app opens, serve index-prod.html - if (reqPath === '/') { - reqPath = '/index-prod.html'; - } - // path.basename returns the last portion of a path which includes filename we want to serve - const reqFilename = path.basename(reqPath); - // use fs module to read index-prod.html (reqPath) in dist folder - fs.readFile(path.join(DIST_PATH, reqPath), (err, data) => { - const mimeType = mime(reqFilename); // returns the file type - // check if there is no error and file type is valid, pass on the info to the next middleware - if (!err && mimeType !== null) { - next({ - mimeType, - charset: charset(mimeType), - data - }); - } else { - console.error(err); - } - }); -} -export { scheme, requestHandler }; +/* + @desc: register a custom protocol and specify file that will be served on request to the origin '/'. our app will be served from 'app://...' instead of the default 'file://...' + @exports: scheme, requestHandler + @usage: is used in main.js + */ + +const { stringify } = require('querystring'); + +import * as fs from 'fs'; +import * as path from 'path'; + +const DIST_PATH = path.join(__dirname, '../../app/dist'); + +const scheme = 'app'; // it will serve resources like app://..... instead of default file://... + +const mimeTypes: Record = { + '.js': 'text/javascript', + '.mjs': 'text/javascript', + '.html': 'text/html', + '.htm': 'text/html', + '.json': 'application/json', + '.css': 'text/css', + '.svg': 'application/svg+xml', + '.ico': 'image/vnd.microsoft.icon', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.map': 'text/plain' +}; + +function charset(mimeType: string): string | null { + return ['.html', '.htm', '.js', '.mjs'].some((m) => m === mimeType) + ? 'utf-8' + : null; +} +// return the file type +function mime(filename: string): string | null { + const type = mimeTypes[path.extname(`${filename || ''}`).toLowerCase()]; + return type || null; +} + +/* requestHandler + servers index-prod.html when we access the root endpoint '/' + read the file above and pass on an object includes mimeType, charset, and exisiting data read from the file +*/ +function requestHandler( + req: Electron.ProtocolRequest, + next: (response: Electron.ProtocolResponse) => void +): void { + // The URL() constructor returns a newly created URL object representing the URL defined by the parameters. + const reqUrl = new URL(req.url); + // path.normalize resolves '..' and '.' segments in sequential path segments + // url.pathname: an initial '/' followed by the path of the URL not including the query string or fragment (or the empty string if there is no path). + let reqPath = path.normalize(reqUrl.pathname); + + // when app opens, serve index-prod.html + if (reqPath === '/') { + reqPath = '/index-prod.html'; + } + // path.basename returns the last portion of a path which includes filename we want to serve + const reqFilename = path.basename(reqPath); + // use fs module to read index-prod.html (reqPath) in dist folder + fs.readFile(path.join(DIST_PATH, reqPath), (err, data) => { + const mimeType = mime(reqFilename); // returns the file type + // check if there is no error and file type is valid, pass on the info to the next middleware + if (!err && mimeType !== null) { + next({ + mimeType, + charset: charset(mimeType), + data + }); + } else { + console.error(err); + } + }); +} +export { scheme, requestHandler }; diff --git a/app/.electron/render.ts b/app/.electron/render.ts index b956f920a..1cdf66de9 100644 --- a/app/.electron/render.ts +++ b/app/.electron/render.ts @@ -1,17 +1,17 @@ - -import {remote} from 'electron'; -import {BrowserWindow} from 'electron-window-manager'; - -const win2 = browserwindow.createNew('win2', 'Windows #2'); -win2.setURL('/win2.html'); -win2.onReady(() => {...}); -win2.open() - - - -/* ; */ + +import {remote} from 'electron'; +import {BrowserWindow} from 'electron-window-manager'; + +const win2 = browserwindow.createNew('win2', 'Windows #2'); +win2.setURL('/win2.html'); +win2.onReady(() => {...}); +win2.open() + + + +/* ; */ diff --git a/app/src/Dashboard/NavbarDash.tsx b/app/src/Dashboard/NavbarDash.tsx index 328c0ec81..7e854600d 100644 --- a/app/src/Dashboard/NavbarDash.tsx +++ b/app/src/Dashboard/NavbarDash.tsx @@ -1,206 +1,206 @@ -import React, { useState, useContext } from 'react'; -import { Theme } from '@mui/material/styles'; -import withStyles from '@mui/styles/withStyles'; -import createStyles from '@mui/styles/createStyles'; -import makeStyles from '@mui/styles/makeStyles'; -import AppBar from '@mui/material/AppBar'; -import Avatar from '@mui/material/Avatar'; -import Brightness3Icon from '@mui/icons-material/Brightness3'; -import Brightness5Icon from '@mui/icons-material/Brightness5'; -import Button from '@mui/material/Button'; -import EventNoteIcon from '@mui/icons-material/EventNote'; -import HomeIcon from '@mui/icons-material/Home'; -import Toolbar from '@mui/material/Toolbar'; -import Typography from '@mui/material/Typography'; -import { Link } from 'react-router-dom'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import SortIcon from '@mui/icons-material/Sort'; -import StarBorderIcon from '@mui/icons-material/StarBorder'; -import PersonIcon from '@mui/icons-material/Person'; -import greenLogo from '../public/icons/png/512x512.png'; -import { setStyle } from '../redux/reducers/slice/styleSlice'; -import { useSelector, useDispatch } from 'react-redux'; - -// NavBar text and button styling -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - flexGrow: 1, - width: '100%' - }, - menuButton: { - marginRight: theme.spacing(2), - color: 'white' - }, - title: { - flexGrow: 1, - color: 'white' - }, - manageProject: { - display: 'flex', - justifyContent: 'center' - } - }) -); -// sorting options -const sortMethods = ['RATING', 'DATE', 'USER']; -// Drop down menu button for SORT PROJECTS -const StyledMenu = withStyles({ - paper: { - border: '1px solid #d3d4d5' - } -})((props) => ( - -)); -const StyledMenuItem = withStyles((theme) => ({ - root: { - '&:focus': { - '& .MuiListItemIcon-root, & .MuiListItemText-primary': { - color: theme.palette.common.white - } - } - } -}))(MenuItem); -// TO DO: set types of props validation -export default function NavBar(props) { - // TO DO: import setStyle - const classes = useStyles(); - const style = useSelector((store) => store.styleSlice); - const dispatch = useDispatch(); - const toggling = () => setIsOpen(!isOpen); - // toggle to open and close dropdown sorting menu - const [isOpen, setIsOpen] = useState(false); - // State for sort projects button - const [anchorEl, setAnchorEl] = React.useState(null); - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setAnchorEl(null); - }; - return ( -
- - - - - - ReacType - - -
- - - {sortMethods.map((option, index) => ( - { - props.optionClicked(option); - toggling(); - handleClose(); - }} - variant="contained" - color="primary" - style={{ minWidth: '137.69px' }} - className={classes.manageProject} - key={index} - > - - - ))} - -
- -
- - - -
-
-
-
- ); -} +import React, { useState, useContext } from 'react'; +import { Theme } from '@mui/material/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; +import makeStyles from '@mui/styles/makeStyles'; +import AppBar from '@mui/material/AppBar'; +import Avatar from '@mui/material/Avatar'; +import Brightness3Icon from '@mui/icons-material/Brightness3'; +import Brightness5Icon from '@mui/icons-material/Brightness5'; +import Button from '@mui/material/Button'; +import EventNoteIcon from '@mui/icons-material/EventNote'; +import HomeIcon from '@mui/icons-material/Home'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { Link } from 'react-router-dom'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import SortIcon from '@mui/icons-material/Sort'; +import StarBorderIcon from '@mui/icons-material/StarBorder'; +import PersonIcon from '@mui/icons-material/Person'; +import greenLogo from '../public/icons/png/512x512.png'; +import { setStyle } from '../redux/reducers/slice/styleSlice'; +import { useSelector, useDispatch } from 'react-redux'; + +// NavBar text and button styling +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + flexGrow: 1, + width: '100%' + }, + menuButton: { + marginRight: theme.spacing(2), + color: 'white' + }, + title: { + flexGrow: 1, + color: 'white' + }, + manageProject: { + display: 'flex', + justifyContent: 'center' + } + }) +); +// sorting options +const sortMethods = ['RATING', 'DATE', 'USER']; +// Drop down menu button for SORT PROJECTS +const StyledMenu = withStyles({ + paper: { + border: '1px solid #d3d4d5' + } +})((props) => ( + +)); +const StyledMenuItem = withStyles((theme) => ({ + root: { + '&:focus': { + '& .MuiListItemIcon-root, & .MuiListItemText-primary': { + color: theme.palette.common.white + } + } + } +}))(MenuItem); +// TO DO: set types of props validation +export default function NavBar(props) { + // TO DO: import setStyle + const classes = useStyles(); + const style = useSelector((store) => store.styleSlice); + const dispatch = useDispatch(); + const toggling = () => setIsOpen(!isOpen); + // toggle to open and close dropdown sorting menu + const [isOpen, setIsOpen] = useState(false); + // State for sort projects button + const [anchorEl, setAnchorEl] = React.useState(null); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + return ( +
+ + + + + + ReacType + + +
+ + + {sortMethods.map((option, index) => ( + { + props.optionClicked(option); + toggling(); + handleClose(); + }} + variant="contained" + color="primary" + style={{ minWidth: '137.69px' }} + className={classes.manageProject} + key={index} + > + + + ))} + +
+ +
+ + + +
+
+
+
+ ); +} diff --git a/app/src/Dashboard/Project.tsx b/app/src/Dashboard/Project.tsx index fb91843ec..a477f2e72 100644 --- a/app/src/Dashboard/Project.tsx +++ b/app/src/Dashboard/Project.tsx @@ -1,248 +1,248 @@ -import React, { useState } from 'react'; -import { useMutation } from '@apollo/client'; -import { - ADD_LIKE, - MAKE_COPY, - DELETE_PROJECT, - PUBLISH_PROJECT, - ADD_COMMENT -} from './gqlStrings'; -import CloseIcon from '@mui/icons-material/Close'; -import AddCommentIcon from '@mui/icons-material/AddComment'; -import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt'; -import GetAppIcon from '@mui/icons-material/GetApp'; -import IconButton from '@mui/material/IconButton'; -import PublishIcon from '@mui/icons-material/Publish'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import createModal from '../components/right/createModal'; -// Variable validation using typescript -type props = { - name: string; - id: string; - userId: string; - username: string; - likes: number; - published: boolean; - comments: object[]; -}; - -// Use current user info to make a make copy of another user's project -const currUserSSID = window.localStorage.getItem('ssid') || 'unavailable'; -const currUsername = window.localStorage.getItem('username') || 'unavailable'; - -const Project = ({ - name, - likes, - id, - username, - published, - comments -}: props): JSX.Element => { - // IMPORTANT: - // 1) schema change projId => id to allows Apollo Client cache auto-update. Only works with 'id' - // 2) always request the 'id' in a mutation request - const [commentVal, setCommentVal] = useState(''); - const [modal, setModal] = useState(null); - const [addLike] = useMutation(ADD_LIKE); - const [makeCopy] = useMutation(MAKE_COPY); - const [deleteProject] = useMutation(DELETE_PROJECT); - const [publishProject] = useMutation(PUBLISH_PROJECT); - const [addComment] = useMutation(ADD_COMMENT); - - const noPointer = { cursor: 'default' }; - //Likes the project when the star icon is clicked - function handleLike(e) { - e.preventDefault(); - const myVar = { - variables: { - projId: id, - likes: likes + 1 - } - }; - addLike(myVar); - } - //Makes a copy of the public project and saves as a user project - function handleDownload(e) { - e.preventDefault(); - const myVar = { - variables: { - projId: id, - userId: currUserSSID, - username: currUsername - } - }; - makeCopy(myVar); - } - //Publishes project from user dashboard to public dashboard - function handlePublish(e) { - e.preventDefault(); - const myVar = { - variables: { - projId: id, - published: !published - } - }; - publishProject(myVar); - } - //Adds the comment to the project - function handleComment(e) { - e.preventDefault(); - const myVar = { - variables: { - projId: id, - comment: commentVal, - username: currUsername - } - }; - addComment(myVar); - } - //sets state of commentVal to what the user types in to comment - function handleChange(e) { - e.preventDefault(); - let commentValue = e.target.value; - setCommentVal(commentValue); - } - const recentComments = []; - if (comments?.length > 0) { - const reversedCommentArray = comments.slice(0).reverse(); - const min = Math.min(6, reversedCommentArray.length); - for (let i = 0; i < min; i++) { - recentComments.push( -

- {reversedCommentArray[i].username}: - {reversedCommentArray[i].text} -

- ); - } - } - // Closes out the open modal - const closeModal = () => setModal(''); - // Creates modal that asks if user wants to delete project - const deleteProjectModal = () => { - //Deletes project from the database - const handleDelete = (e) => { - e.preventDefault(); - const myVar = { - variables: { - projId: id - } - }; - deleteProject(myVar); - }; - // Set modal options - const children = ( - - - - - - ); - - // Create modal - setModal( - createModal({ - closeModal, - children, - message: 'Are you sure you want to delete this project?', - primBtnLabel: null, - primBtnAction: null, - secBtnAction: null, - secBtnLabel: null, - open: true - }) - ); - }; - - return ( -
-
- {currUsername === username ? ( - - - - ) : ( - '' - )} -
- -

Project: {name}

-

Author: {username}

-

Likes: {likes}

-
-
- -
- - - - {currUsername !== username ? ( - - - - ) : ( - '' - )} - {currUsername === username ? ( - - - - ) : ( - '' - )} -
-
-
{recentComments}
-
- - -
- {modal} -
- ); -}; -export default Project; +import React, { useState } from 'react'; +import { useMutation } from '@apollo/client'; +import { + ADD_LIKE, + MAKE_COPY, + DELETE_PROJECT, + PUBLISH_PROJECT, + ADD_COMMENT +} from './gqlStrings'; +import CloseIcon from '@mui/icons-material/Close'; +import AddCommentIcon from '@mui/icons-material/AddComment'; +import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt'; +import GetAppIcon from '@mui/icons-material/GetApp'; +import IconButton from '@mui/material/IconButton'; +import PublishIcon from '@mui/icons-material/Publish'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import createModal from '../components/right/createModal'; +// Variable validation using typescript +type props = { + name: string; + id: string; + userId: string; + username: string; + likes: number; + published: boolean; + comments: object[]; +}; + +// Use current user info to make a make copy of another user's project +const currUserSSID = window.localStorage.getItem('ssid') || 'unavailable'; +const currUsername = window.localStorage.getItem('username') || 'unavailable'; + +const Project = ({ + name, + likes, + id, + username, + published, + comments +}: props): JSX.Element => { + // IMPORTANT: + // 1) schema change projId => id to allows Apollo Client cache auto-update. Only works with 'id' + // 2) always request the 'id' in a mutation request + const [commentVal, setCommentVal] = useState(''); + const [modal, setModal] = useState(null); + const [addLike] = useMutation(ADD_LIKE); + const [makeCopy] = useMutation(MAKE_COPY); + const [deleteProject] = useMutation(DELETE_PROJECT); + const [publishProject] = useMutation(PUBLISH_PROJECT); + const [addComment] = useMutation(ADD_COMMENT); + + const noPointer = { cursor: 'default' }; + //Likes the project when the star icon is clicked + function handleLike(e) { + e.preventDefault(); + const myVar = { + variables: { + projId: id, + likes: likes + 1 + } + }; + addLike(myVar); + } + //Makes a copy of the public project and saves as a user project + function handleDownload(e) { + e.preventDefault(); + const myVar = { + variables: { + projId: id, + userId: currUserSSID, + username: currUsername + } + }; + makeCopy(myVar); + } + //Publishes project from user dashboard to public dashboard + function handlePublish(e) { + e.preventDefault(); + const myVar = { + variables: { + projId: id, + published: !published + } + }; + publishProject(myVar); + } + //Adds the comment to the project + function handleComment(e) { + e.preventDefault(); + const myVar = { + variables: { + projId: id, + comment: commentVal, + username: currUsername + } + }; + addComment(myVar); + } + //sets state of commentVal to what the user types in to comment + function handleChange(e) { + e.preventDefault(); + let commentValue = e.target.value; + setCommentVal(commentValue); + } + const recentComments = []; + if (comments?.length > 0) { + const reversedCommentArray = comments.slice(0).reverse(); + const min = Math.min(6, reversedCommentArray.length); + for (let i = 0; i < min; i++) { + recentComments.push( +

+ {reversedCommentArray[i].username}: + {reversedCommentArray[i].text} +

+ ); + } + } + // Closes out the open modal + const closeModal = () => setModal(''); + // Creates modal that asks if user wants to delete project + const deleteProjectModal = () => { + //Deletes project from the database + const handleDelete = (e) => { + e.preventDefault(); + const myVar = { + variables: { + projId: id + } + }; + deleteProject(myVar); + }; + // Set modal options + const children = ( + + + + + + ); + + // Create modal + setModal( + createModal({ + closeModal, + children, + message: 'Are you sure you want to delete this project?', + primBtnLabel: null, + primBtnAction: null, + secBtnAction: null, + secBtnLabel: null, + open: true + }) + ); + }; + + return ( +
+
+ {currUsername === username ? ( + + + + ) : ( + '' + )} +
+ +

Project: {name}

+

Author: {username}

+

Likes: {likes}

+
+
+ +
+ + + + {currUsername !== username ? ( + + + + ) : ( + '' + )} + {currUsername === username ? ( + + + + ) : ( + '' + )} +
+
+
{recentComments}
+
+ + +
+ {modal} +
+ ); +}; +export default Project; diff --git a/app/src/Dashboard/ProjectContainer.tsx b/app/src/Dashboard/ProjectContainer.tsx index 931ae2b0b..ef486b2df 100644 --- a/app/src/Dashboard/ProjectContainer.tsx +++ b/app/src/Dashboard/ProjectContainer.tsx @@ -1,190 +1,190 @@ -import React, { useState} from 'react'; -import { ThemeProvider, Theme, StyledEngineProvider, useTheme } from '@mui/material/styles'; -import makeStyles from '@mui/styles/makeStyles'; -import { useQuery } from '@apollo/client'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; -import Box from '@mui/material/Box'; -import { GET_PROJECTS } from './gqlStrings'; -import Project from './Project'; -import NavBarDash from './NavbarDash'; -import { useSelector } from 'react-redux'; -import { theme1, theme2 } from '../public/styles/theme'; - - -declare module '@mui/styles/defaultTheme' { - interface DefaultTheme extends Theme {} -} - - -// Implement Apollo Client useQuery hook to retrieve data from the server through graphQL. This includes 2 steps: -// 1) Impliment Apollo Provider in the top component in ./src/index.js, this allows children components access to the queried data -// 2) useQuery hook will update the data stored in Apollo Client's cache and automatically trigger child components rendering - - -// setting light and dark themes (navbar and background); linked to theme.ts -const lightTheme = theme1; -const darkTheme = theme2; // dark mode color in theme.ts not reached - -const arrToComponent = arr => - arr.map((proj, index) => ( - - )); - -// Start Pulled from materialUI to create a tab panel -const a11yProps = (index: any) => ({ - id: `vertical-tab-${index}`, - 'aria-controls': `vertical-tabpanel-${index}` -}); - -interface LinkTabProps { - label?: string; - href?: string; -} -const LinkTab = (props: LinkTabProps) => ( - ) => { - event.preventDefault(); - }} - {...props} - /> -); -const TabPanelItem = (props: TabPanelProps): JSX.Element => { - const theme = useTheme(); - const { children, index, value, ...other } = props; - return ( - - ); -}; - -const useStyles = makeStyles(theme => ({ - root: { - flexGrow: 1, - // backgroundColor: theme.palette.background.paper, - display: 'flex' - }, - tabs: { - // borderRight: `1px solid ${theme.palette.divider}` - } -})); -// End of prefab code to generate a tab panel -const ProjectContainer = (): JSX.Element => { - const classes = useStyles(); - const [value, setValue] = useState(0); - const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setValue(newValue); - }; - // old code from project container - const myVar = {}; - // Need this for the individual user dasboard, for now, dashboard shows all projects from all users - const userSSID = window.localStorage.getItem('ssid') || 'unavailable'; - const username = window.localStorage.getItem('username') || 'unavailable'; - const [isThemeLight, setTheme] = useState(true); - const style = useSelector(store => store.styleSlice) - // hook for sorting menu - const [selectedOption, setSelectedOption] = useState('RATING'); - const sortByRating = projects => { - // generate a sorted array of public projects based on likes - const sortedRatings = projects.sort((a, b) => b.likes - a.likes); - return sortedRatings; - }; - const sortByDate = projects => { - // generate a sorted array of public projects based on date - const sortedRatings = projects.sort((a, b) => b.createdAt - a.createdAt); - return sortedRatings; - }; - const sortByUser = projects => { - // generate a sorted array of public projects based on username - const sortedRatings = projects.sort((a, b) => b.username - a.username); - return sortedRatings; - }; - // function for selecting drop down sorting menu - const optionClicked = value => { - setSelectedOption(value); - }; - // useQuery hook abstracts fetch request - const { loading, error, data } = useQuery(GET_PROJECTS, { - pollInterval: 2000, - variables: myVar - }); - if (loading) return

Loading...

; - if (error) return

Error :{error}

; - // based on resolver(getAllProject) for this query, the data is stored in the data object with the key 'getAllProjects' - const projects = data.getAllProjects; - - //create array to hold the data recieved in the public dashboard the will be conditionally rendered - let sortedProjects = projects.filter(proj => { - return proj.published; - }); - const userProjects = projects.filter(proj => { - return proj.username === username; - }); - // checking which sorting method was selected from drop down menu and invoking correct sorting function - if (selectedOption === 'DATE') sortedProjects = sortByDate(sortedProjects); - else if (selectedOption === 'USER') - sortedProjects = sortByUser(sortedProjects); - else if (selectedOption === 'RATING') - sortedProjects = sortByRating(sortedProjects); - // create array to hold the components Project of loggin-in users - // generate an array of Project components based on queried data - const userDisplay = arrToComponent(userProjects); - // create an array of components Project that will be conditionally rendered - const sortedDisplay = arrToComponent(sortedProjects); - // old code from Project Container - return ( -
- - -
- -
- - - - - -

Shared Dashboard

-
{sortedDisplay}
-
- -

Private Dashboard

-
{userDisplay}
-
-
-
-
-
-
- ); -}; -export default ProjectContainer; +import React, { useState} from 'react'; +import { ThemeProvider, Theme, StyledEngineProvider, useTheme } from '@mui/material/styles'; +import makeStyles from '@mui/styles/makeStyles'; +import { useQuery } from '@apollo/client'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Box from '@mui/material/Box'; +import { GET_PROJECTS } from './gqlStrings'; +import Project from './Project'; +import NavBarDash from './NavbarDash'; +import { useSelector } from 'react-redux'; +import { theme1, theme2 } from '../public/styles/theme'; + + +declare module '@mui/styles/defaultTheme' { + interface DefaultTheme extends Theme {} +} + + +// Implement Apollo Client useQuery hook to retrieve data from the server through graphQL. This includes 2 steps: +// 1) Impliment Apollo Provider in the top component in ./src/index.js, this allows children components access to the queried data +// 2) useQuery hook will update the data stored in Apollo Client's cache and automatically trigger child components rendering + + +// setting light and dark themes (navbar and background); linked to theme.ts +const lightTheme = theme1; +const darkTheme = theme2; // dark mode color in theme.ts not reached + +const arrToComponent = arr => + arr.map((proj, index) => ( + + )); + +// Start Pulled from materialUI to create a tab panel +const a11yProps = (index: any) => ({ + id: `vertical-tab-${index}`, + 'aria-controls': `vertical-tabpanel-${index}` +}); + +interface LinkTabProps { + label?: string; + href?: string; +} +const LinkTab = (props: LinkTabProps) => ( + ) => { + event.preventDefault(); + }} + {...props} + /> +); +const TabPanelItem = (props: TabPanelProps): JSX.Element => { + const theme = useTheme(); + const { children, index, value, ...other } = props; + return ( + + ); +}; + +const useStyles = makeStyles(theme => ({ + root: { + flexGrow: 1, + // backgroundColor: theme.palette.background.paper, + display: 'flex' + }, + tabs: { + // borderRight: `1px solid ${theme.palette.divider}` + } +})); +// End of prefab code to generate a tab panel +const ProjectContainer = (): JSX.Element => { + const classes = useStyles(); + const [value, setValue] = useState(0); + const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setValue(newValue); + }; + // old code from project container + const myVar = {}; + // Need this for the individual user dasboard, for now, dashboard shows all projects from all users + const userSSID = window.localStorage.getItem('ssid') || 'unavailable'; + const username = window.localStorage.getItem('username') || 'unavailable'; + const [isThemeLight, setTheme] = useState(true); + const style = useSelector(store => store.styleSlice) + // hook for sorting menu + const [selectedOption, setSelectedOption] = useState('RATING'); + const sortByRating = projects => { + // generate a sorted array of public projects based on likes + const sortedRatings = projects.sort((a, b) => b.likes - a.likes); + return sortedRatings; + }; + const sortByDate = projects => { + // generate a sorted array of public projects based on date + const sortedRatings = projects.sort((a, b) => b.createdAt - a.createdAt); + return sortedRatings; + }; + const sortByUser = projects => { + // generate a sorted array of public projects based on username + const sortedRatings = projects.sort((a, b) => b.username - a.username); + return sortedRatings; + }; + // function for selecting drop down sorting menu + const optionClicked = value => { + setSelectedOption(value); + }; + // useQuery hook abstracts fetch request + const { loading, error, data } = useQuery(GET_PROJECTS, { + pollInterval: 2000, + variables: myVar + }); + if (loading) return

Loading...

; + if (error) return

Error :{error}

; + // based on resolver(getAllProject) for this query, the data is stored in the data object with the key 'getAllProjects' + const projects = data.getAllProjects; + + //create array to hold the data recieved in the public dashboard the will be conditionally rendered + let sortedProjects = projects.filter(proj => { + return proj.published; + }); + const userProjects = projects.filter(proj => { + return proj.username === username; + }); + // checking which sorting method was selected from drop down menu and invoking correct sorting function + if (selectedOption === 'DATE') sortedProjects = sortByDate(sortedProjects); + else if (selectedOption === 'USER') + sortedProjects = sortByUser(sortedProjects); + else if (selectedOption === 'RATING') + sortedProjects = sortByRating(sortedProjects); + // create array to hold the components Project of loggin-in users + // generate an array of Project components based on queried data + const userDisplay = arrToComponent(userProjects); + // create an array of components Project that will be conditionally rendered + const sortedDisplay = arrToComponent(sortedProjects); + // old code from Project Container + return ( +
+ + +
+ +
+ + + + + +

Shared Dashboard

+
{sortedDisplay}
+
+ +

Private Dashboard

+
{userDisplay}
+
+
+
+
+
+
+ ); +}; +export default ProjectContainer; diff --git a/app/src/Dashboard/gqlStrings.ts b/app/src/Dashboard/gqlStrings.ts index 97ec1a082..680623f5a 100644 --- a/app/src/Dashboard/gqlStrings.ts +++ b/app/src/Dashboard/gqlStrings.ts @@ -1,58 +1,58 @@ -import { gql } from '@apollo/client'; -// Query -export const GET_PROJECTS = gql`query GetAllProjects($userId: ID) { - getAllProjects(userId: $userId) { - name - likes - id - userId - username - published - comments { - username - text - } - } -}`; - -// Mutation -export const ADD_LIKE = gql` - mutation AddLike($projId: ID!, $likes: Int!) { - addLike(projId: $projId, likes: $likes) - { - id - } - }`; - -export const MAKE_COPY = gql` - mutation MakeCopy ($userId: ID!, $projId: ID!, $username: String!) { - makeCopy(userId: $userId, projId: $projId, username: $username) - { - id - } - }`; - -export const DELETE_PROJECT = gql` - mutation DeleteProject($projId: ID!) { - deleteProject(projId: $projId) - { - id - } - }`; - -export const PUBLISH_PROJECT = gql` - mutation Publish($projId: ID!, $published: Boolean!) { - publishProject(projId: $projId, published: $published) - { - id - published - } - }`; - - export const ADD_COMMENT = gql` - mutation AddComment($projId: ID!, $comment: String!, $username: String!) { - addComment(projId: $projId, comment: $comment, username: $username) - { - id - } - }`; +import { gql } from '@apollo/client'; +// Query +export const GET_PROJECTS = gql`query GetAllProjects($userId: ID) { + getAllProjects(userId: $userId) { + name + likes + id + userId + username + published + comments { + username + text + } + } +}`; + +// Mutation +export const ADD_LIKE = gql` + mutation AddLike($projId: ID!, $likes: Int!) { + addLike(projId: $projId, likes: $likes) + { + id + } + }`; + +export const MAKE_COPY = gql` + mutation MakeCopy ($userId: ID!, $projId: ID!, $username: String!) { + makeCopy(userId: $userId, projId: $projId, username: $username) + { + id + } + }`; + +export const DELETE_PROJECT = gql` + mutation DeleteProject($projId: ID!) { + deleteProject(projId: $projId) + { + id + } + }`; + +export const PUBLISH_PROJECT = gql` + mutation Publish($projId: ID!, $published: Boolean!) { + publishProject(projId: $projId, published: $published) + { + id + published + } + }`; + + export const ADD_COMMENT = gql` + mutation AddComment($projId: ID!, $comment: String!, $username: String!) { + addComment(projId: $projId, comment: $comment, username: $username) + { + id + } + }`; diff --git a/app/src/Dashboard/styles.css b/app/src/Dashboard/styles.css index 89500f729..95e45bd0a 100644 --- a/app/src/Dashboard/styles.css +++ b/app/src/Dashboard/styles.css @@ -1,91 +1,91 @@ -.project { - display: flex; - flex-direction: column; - align-items: stretch; - margin: 5px; - border: 1px solid #f0f0f0; - border-radius: 5px; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); - height: 500px; - width: 400px; - justify-content: space-between; -} - -.dashboardContainer { - height: 100%; - width: 100%; -} - -.userDashboard { - display: flex; - flex-direction: row; -} - -.projectPanel { - width: 100%; -} - -.projectContainer { - display: flex; - flex-direction: column-reverse; - flex-flow: row wrap; - flex-grow: 1; - overflow-y: scroll; -} - -.projectInfo { - text-align: center; -} - -.commentContainer { - text-align: center; - height: 400px; - overflow-y: scroll; -} - -.commentBtn { - color: #bebebed8; -} - -.comment { - font-size: 125%; -} - -.renderedCom { - text-align: center; -} - -.commentInput { - display: flex; - height: 100px; - width: 100%; - border: 1px solid #f0f0f0; - position: relative; -} - -.commentInput > * { - flex: auto; -} - -.icons { - width: 100%; - display: flex; - justify-content: center; - align-items: center; -} - -.header { - background-color: #0671e3; - color: rgba(255, 255, 255, 0.897); - width: 100%; - position: relative; -} - -.commentField { - border: 1px solid #f0f0f0; - padding-left: 2%; -} - -h1 { - text-align: center; -} +.project { + display: flex; + flex-direction: column; + align-items: stretch; + margin: 5px; + border: 1px solid #f0f0f0; + border-radius: 5px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); + height: 500px; + width: 400px; + justify-content: space-between; +} + +.dashboardContainer { + height: 100%; + width: 100%; +} + +.userDashboard { + display: flex; + flex-direction: row; +} + +.projectPanel { + width: 100%; +} + +.projectContainer { + display: flex; + flex-direction: column-reverse; + flex-flow: row wrap; + flex-grow: 1; + overflow-y: scroll; +} + +.projectInfo { + text-align: center; +} + +.commentContainer { + text-align: center; + height: 400px; + overflow-y: scroll; +} + +.commentBtn { + color: #bebebed8; +} + +.comment { + font-size: 125%; +} + +.renderedCom { + text-align: center; +} + +.commentInput { + display: flex; + height: 100px; + width: 100%; + border: 1px solid #f0f0f0; + position: relative; +} + +.commentInput > * { + flex: auto; +} + +.icons { + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.header { + background-color: #0671e3; + color: rgba(255, 255, 255, 0.897); + width: 100%; + position: relative; +} + +.commentField { + border: 1px solid #f0f0f0; + padding-left: 2%; +} + +h1 { + text-align: center; +} diff --git a/app/src/components/App.tsx b/app/src/components/App.tsx index f97e98a27..d4b65db9e 100644 --- a/app/src/components/App.tsx +++ b/app/src/components/App.tsx @@ -1,24 +1,24 @@ -import '../public/styles/style.css'; - -import React, { useEffect } from 'react'; -import { toggleLoggedIn } from '../redux/reducers/slice/appStateSlice'; -import { useDispatch } from 'react-redux'; - -import AppContainer from '../containers/AppContainer'; - -export const App: React.FC = (): JSX.Element => { - const dispatch = useDispatch(); - useEffect(() => { - if (window.localStorage.getItem('ssid') !== 'guest') { - dispatch(toggleLoggedIn(true)); - } - }, []); - - return ( -
- -
- ); -}; - -export default App; +import '../public/styles/style.css'; + +import React, { useEffect } from 'react'; +import { toggleLoggedIn } from '../redux/reducers/slice/appStateSlice'; +import { useDispatch } from 'react-redux'; + +import AppContainer from '../containers/AppContainer'; + +export const App: React.FC = (): JSX.Element => { + const dispatch = useDispatch(); + useEffect(() => { + if (window.localStorage.getItem('ssid') !== 'guest') { + dispatch(toggleLoggedIn(true)); + } + }, []); + + return ( +
+ +
+ ); +}; + +export default App; diff --git a/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx b/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx index ff1d5e4f4..8d8f5804a 100644 --- a/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/AssignContainer.tsx @@ -1,140 +1,140 @@ -import React, { useState, Fragment } from 'react'; -import DataTable from '../CreateTab/components/DataTable'; -import ContextDropDown from './components/ContextDropDown'; -import ComponentDropDown from './components/ComponentDropDrown'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid'; -import ComponentTable from './components/ComponentTable'; -import { Button } from '@mui/material'; -import DoubleArrowIcon from '@mui/icons-material/DoubleArrow'; -import { addComponentToContext } from '../../../redux/reducers/slice/contextReducer'; -import { useSelector, useDispatch } from 'react-redux'; -import { deleteElement } from '../../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../../redux/store'; -import { emitEvent } from '../../../../src/helperFunctions/socket'; - -const AssignContainer = () => { - const dispatch = useDispatch(); - const defaultTableData = [{ key: 'Key', value: 'Value' }]; - const [tableState, setTableState] = React.useState(defaultTableData); - const [contextInput, setContextInput] = React.useState(null); - const [componentInput, setComponentInput] = React.useState(null); - const [componentTable, setComponentTable] = useState([]); - const { state, contextParam } = useSelector((store: RootState) => ({ - state: store.appState, - contextParam: store.contextSlice - })); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - //sets table data if it exists - const renderTable = (targetContext) => { - targetContext?.values && setTableState(targetContext.values); - }; - - //construct data for table displaying component table - const renderComponentTable = (targetComponent) => { - //target Component is main - const listOfContexts = []; - if (!Array.isArray(state) && targetComponent?.name) { - contextParam.allContext.forEach((context) => { - if (context.components.includes(targetComponent.name)) { - listOfContexts.push(context.name); - } - }); - setComponentTable(listOfContexts); - } - }; - - //handling assignment of contexts to components - const handleAssignment = () => { - if ( - contextInput === '' || - contextInput === null || - componentInput === '' || - componentInput === null - ) - return; - - dispatch( - addComponentToContext({ - context: contextInput, - component: componentInput - }) - ); - //trigger generateCode(), update code preview tab - dispatch(deleteElement({ id: 'FAKE_ID', contextParam: contextParam })); - - if (roomCode) { - emitEvent('assignContextActions', roomCode, { - context: contextInput, - component: componentInput, - id: 'FAKE_ID', - contextParam: contextParam - }); - } - - renderComponentTable(componentInput); - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default AssignContainer; +import React, { useState, Fragment } from 'react'; +import DataTable from '../CreateTab/components/DataTable'; +import ContextDropDown from './components/ContextDropDown'; +import ComponentDropDown from './components/ComponentDropDrown'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import ComponentTable from './components/ComponentTable'; +import { Button } from '@mui/material'; +import DoubleArrowIcon from '@mui/icons-material/DoubleArrow'; +import { addComponentToContext } from '../../../redux/reducers/slice/contextReducer'; +import { useSelector, useDispatch } from 'react-redux'; +import { deleteElement } from '../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../redux/store'; +import { emitEvent } from '../../../../src/helperFunctions/socket'; + +const AssignContainer = () => { + const dispatch = useDispatch(); + const defaultTableData = [{ key: 'Key', value: 'Value' }]; + const [tableState, setTableState] = React.useState(defaultTableData); + const [contextInput, setContextInput] = React.useState(null); + const [componentInput, setComponentInput] = React.useState(null); + const [componentTable, setComponentTable] = useState([]); + const { state, contextParam } = useSelector((store: RootState) => ({ + state: store.appState, + contextParam: store.contextSlice + })); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + //sets table data if it exists + const renderTable = (targetContext) => { + targetContext?.values && setTableState(targetContext.values); + }; + + //construct data for table displaying component table + const renderComponentTable = (targetComponent) => { + //target Component is main + const listOfContexts = []; + if (!Array.isArray(state) && targetComponent?.name) { + contextParam.allContext.forEach((context) => { + if (context.components.includes(targetComponent.name)) { + listOfContexts.push(context.name); + } + }); + setComponentTable(listOfContexts); + } + }; + + //handling assignment of contexts to components + const handleAssignment = () => { + if ( + contextInput === '' || + contextInput === null || + componentInput === '' || + componentInput === null + ) + return; + + dispatch( + addComponentToContext({ + context: contextInput, + component: componentInput + }) + ); + //trigger generateCode(), update code preview tab + dispatch(deleteElement({ id: 'FAKE_ID', contextParam: contextParam })); + + if (roomCode) { + emitEvent('assignContextActions', roomCode, { + context: contextInput, + component: componentInput, + id: 'FAKE_ID', + contextParam: contextParam + }); + } + + renderComponentTable(componentInput); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default AssignContainer; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx index b06802471..32f4f8f22 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx @@ -1,104 +1,104 @@ -import React, { Fragment } from 'react'; -import TextField from '@mui/material/TextField'; -import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; -import Box from '@mui/material/Box'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../../../redux/store'; - -const filter = createFilterOptions(); - -const ComponentDropDown = ({ - renderComponentTable, - componentInput, - setComponentInput -}) => { - const { state } = useSelector((store: RootState) => ({ - state: store.appState - })); - - const onChange = (event, newValue) => { - if (typeof newValue === 'string') { - setComponentInput({ - name: newValue - }); - } else if (newValue && newValue.inputValue) { - // Create a new contextInput from the user input - setComponentInput({ - name: newValue.inputValue, - values: [] - }); - renderComponentTable(newValue); - } else { - setComponentInput(newValue); - renderComponentTable(newValue); - } - }; - - const filterOptions = (options, params) => { - const filtered = filter(options, params); - const { inputValue } = params; - // Suggest the creation of a new contextInput - const isExisting = options.some((option) => inputValue === option.name); - if (inputValue !== '' && !isExisting) { - filtered.push({ - inputValue, - name: `Add "${inputValue}"` - }); - } - - return filtered; - }; - - const getOptionLabel = (option) => { - // Value selected with enter, right from the input - if (typeof option === 'string') { - return option; - } - // Add "xxx" option created dynamically - if (option.inputValue) { - return option.inputValue; - } - // Regular option - return option.name; - }; - - const renderOption = (props, option) => ( -
  • - {option.name} -
  • - ); - - return ( - - - ( - - )} - /> - - - ); -}; - -export default ComponentDropDown; +import React, { Fragment } from 'react'; +import TextField from '@mui/material/TextField'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../../redux/store'; + +const filter = createFilterOptions(); + +const ComponentDropDown = ({ + renderComponentTable, + componentInput, + setComponentInput +}) => { + const { state } = useSelector((store: RootState) => ({ + state: store.appState + })); + + const onChange = (event, newValue) => { + if (typeof newValue === 'string') { + setComponentInput({ + name: newValue + }); + } else if (newValue && newValue.inputValue) { + // Create a new contextInput from the user input + setComponentInput({ + name: newValue.inputValue, + values: [] + }); + renderComponentTable(newValue); + } else { + setComponentInput(newValue); + renderComponentTable(newValue); + } + }; + + const filterOptions = (options, params) => { + const filtered = filter(options, params); + const { inputValue } = params; + // Suggest the creation of a new contextInput + const isExisting = options.some((option) => inputValue === option.name); + if (inputValue !== '' && !isExisting) { + filtered.push({ + inputValue, + name: `Add "${inputValue}"` + }); + } + + return filtered; + }; + + const getOptionLabel = (option) => { + // Value selected with enter, right from the input + if (typeof option === 'string') { + return option; + } + // Add "xxx" option created dynamically + if (option.inputValue) { + return option.inputValue; + } + // Regular option + return option.name; + }; + + const renderOption = (props, option) => ( +
  • + {option.name} +
  • + ); + + return ( + + + ( + + )} + /> + + + ); +}; + +export default ComponentDropDown; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx index 76397b480..426780e61 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx @@ -1,52 +1,52 @@ -import React from 'react'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import { styled } from '@mui/material/styles'; -import TableCell, { tableCellClasses } from '@mui/material/TableCell'; - -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: theme.palette.common.white - }, - [`&.${tableCellClasses.body}`]: { - fontSize: 14 - } -})); - -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover - }, - // hide last border - '&:last-child td, &:last-child th': { - border: 0 - } -})); - -export default function DataTable({ target }) { - return ( - - - - - Contexts Consumed - - - - {target.map((data, index) => ( - - - {data} - - - ))} - -
    -
    - ); +import React from 'react'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import { styled } from '@mui/material/styles'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})); + +export default function DataTable({ target }) { + return ( + + + + + Contexts Consumed + + + + {target.map((data, index) => ( + + + {data} + + + ))} + +
    +
    + ); } \ No newline at end of file diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx index f1591ebe4..6ba077992 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx @@ -1,102 +1,102 @@ -import React, { Fragment, useState, useEffect } from 'react'; -import TextField from '@mui/material/TextField'; -import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; -import Box from '@mui/material/Box'; - - -const filter = createFilterOptions(); - -const ContextDropDown = ({ - contextStore, - renderTable, - contextInput, - setContextInput -}) => { - const { allContext } = contextStore; - - const onChange = (event, newValue) => { - if (typeof newValue === 'string') { - setContextInput({ - name: newValue - }); - } else if (newValue && newValue.inputValue) { - // Create a new contextInput from the user input - setContextInput({ - name: newValue.inputValue, - values: [] - }); - renderTable(newValue); - } else { - setContextInput(newValue); - renderTable(newValue); - } - }; - - const filterOptions = (options, params) => { - const filtered = filter(options, params); - const { inputValue } = params; - // Suggest the creation of a new contextInput - const isExisting = options.some((option) => inputValue === option.name); - if (inputValue !== '' && !isExisting) { - filtered.push({ - inputValue, - name: `Add "${inputValue}"` - }); - } - - return filtered; - }; - - const getOptionLabel = (option) => { - // Value selected with enter, right from the input - if (typeof option === 'string') { - return option; - } - // Add "xxx" option created dynamically - if (option.inputValue) { - return option.inputValue; - } - // Regular option - return option.name; - }; - - const renderOption = (props, option) => ( -
  • - {option.name} -
  • - ); - - return ( - - - ( - - )} - /> - - - ); -}; - -export default ContextDropDown; +import React, { Fragment, useState, useEffect } from 'react'; +import TextField from '@mui/material/TextField'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; + + +const filter = createFilterOptions(); + +const ContextDropDown = ({ + contextStore, + renderTable, + contextInput, + setContextInput +}) => { + const { allContext } = contextStore; + + const onChange = (event, newValue) => { + if (typeof newValue === 'string') { + setContextInput({ + name: newValue + }); + } else if (newValue && newValue.inputValue) { + // Create a new contextInput from the user input + setContextInput({ + name: newValue.inputValue, + values: [] + }); + renderTable(newValue); + } else { + setContextInput(newValue); + renderTable(newValue); + } + }; + + const filterOptions = (options, params) => { + const filtered = filter(options, params); + const { inputValue } = params; + // Suggest the creation of a new contextInput + const isExisting = options.some((option) => inputValue === option.name); + if (inputValue !== '' && !isExisting) { + filtered.push({ + inputValue, + name: `Add "${inputValue}"` + }); + } + + return filtered; + }; + + const getOptionLabel = (option) => { + // Value selected with enter, right from the input + if (typeof option === 'string') { + return option; + } + // Add "xxx" option created dynamically + if (option.inputValue) { + return option.inputValue; + } + // Regular option + return option.name; + }; + + const renderOption = (props, option) => ( +
  • + {option.name} +
  • + ); + + return ( + + + ( + + )} + /> + + + ); +}; + +export default ContextDropDown; diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx index 5906b3093..5296d6b9d 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ContextTable.tsx @@ -1,72 +1,72 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell, { tableCellClasses } from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; - -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: theme.palette.common.white - }, - [`&.${tableCellClasses.body}`]: { - fontSize: 14 - } -})); - -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover - }, - // hide last border - '&:last-child td, &:last-child th': { - border: 0 - } -})); - -function createData( - name: string, - calories: number, - fat: number, - carbs: number, - protein: number -) { - return { name, calories, fat, carbs, protein }; -} - -const rows = [ - createData('Frozen yoghurt', 159, 6.0, 24, 4.0), - createData('Ice cream sandwich', 237, 9.0, 37, 4.3), - createData('Eclair', 262, 16.0, 24, 6.0), - createData('Cupcake', 305, 3.7, 67, 4.3), - createData('Gingerbread', 356, 16.0, 49, 3.9) -]; - -export default function ContextTable() { - return ( - - - - - Context - Component - - - - {rows.map((row) => ( - - - {row.name} - - {row.calories} - - ))} - -
    -
    - ); -} +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})); + +function createData( + name: string, + calories: number, + fat: number, + carbs: number, + protein: number +) { + return { name, calories, fat, carbs, protein }; +} + +const rows = [ + createData('Frozen yoghurt', 159, 6.0, 24, 4.0), + createData('Ice cream sandwich', 237, 9.0, 37, 4.3), + createData('Eclair', 262, 16.0, 24, 6.0), + createData('Cupcake', 305, 3.7, 67, 4.3), + createData('Gingerbread', 356, 16.0, 49, 3.9) +]; + +export default function ContextTable() { + return ( + + + + + Context + Component + + + + {rows.map((row) => ( + + + {row.name} + + {row.calories} + + ))} + +
    +
    + ); +} diff --git a/app/src/components/ContextAPIManager/ContextManager.tsx b/app/src/components/ContextAPIManager/ContextManager.tsx index 8927f9dc0..9f3bf7c0b 100644 --- a/app/src/components/ContextAPIManager/ContextManager.tsx +++ b/app/src/components/ContextAPIManager/ContextManager.tsx @@ -1,64 +1,64 @@ -import React, { useContext } from 'react'; -import { makeStyles } from '@mui/styles'; -import Box from '@mui/material/Box'; -import Tab from '@mui/material/Tab'; -import TabContext from '@mui/lab/TabContext'; -import TabList from '@mui/lab/TabList'; -import TabPanel from '@mui/lab/TabPanel'; - -import CreateContainer from './CreateTab/CreateContainer'; -import AssignContainer from './AssignTab/AssignContainer'; -import DisplayContainer from './DisplayTab/DisplayContainer'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../redux/store'; - -const useStyles = makeStyles({ - contextContainer: { - height: 'fit-content' - } -}); - -const ContextManager = (props): JSX.Element => { - const style = useSelector((store: RootState) => store.styleSlice); - const classes = useStyles(); - const [value, setValue] = React.useState('1'); - - const handleChange = (event: React.SyntheticEvent, newValue: string) => { - setValue(newValue); - }; - - const color = 'white'; - - return ( - -
    - - - - - - - - - - - - - - - - - - - - -
    -
    - ); -}; - -export default ContextManager; +import React, { useContext } from 'react'; +import { makeStyles } from '@mui/styles'; +import Box from '@mui/material/Box'; +import Tab from '@mui/material/Tab'; +import TabContext from '@mui/lab/TabContext'; +import TabList from '@mui/lab/TabList'; +import TabPanel from '@mui/lab/TabPanel'; + +import CreateContainer from './CreateTab/CreateContainer'; +import AssignContainer from './AssignTab/AssignContainer'; +import DisplayContainer from './DisplayTab/DisplayContainer'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; + +const useStyles = makeStyles({ + contextContainer: { + height: 'fit-content' + } +}); + +const ContextManager = (props): JSX.Element => { + const style = useSelector((store: RootState) => store.styleSlice); + const classes = useStyles(); + const [value, setValue] = React.useState('1'); + + const handleChange = (event: React.SyntheticEvent, newValue: string) => { + setValue(newValue); + }; + + const color = 'white'; + + return ( + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    + ); +}; + +export default ContextManager; diff --git a/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx b/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx index 2bc87a598..e013cb919 100644 --- a/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/CreateContainer.tsx @@ -1,161 +1,161 @@ -import React from 'react'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid'; -import DataTable from './components/DataTable'; -import AddDataForm from './components/AddDataForm'; -import AddContextForm from './components/AddContextForm'; -import { Typography } from '@mui/material'; -import { - addContext, - deleteContext, - addContextValues -} from '../../../redux/reducers/slice/contextReducer'; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState } from '../../../redux/store'; -import { emitEvent } from '../../../../src/helperFunctions/socket'; - -const CreateContainer = () => { - const state = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const [contextInput, setContextInput] = React.useState(''); - const [currentContext, setCurrentContext] = React.useState(''); - const [errorMsg, setErrorMsg] = React.useState(''); - const [errorStatus, setErrorStatus] = React.useState(false); - const currentKeyValues = state.allContext.find( - (obj) => obj.name === currentContext - )?.values || [{ key: 'Enter Key', value: 'Enter value' }]; - const dispatch = useDispatch(); - - //update data store when user adds a new context - const handleClickSelectContext = () => { - let letters = /[a-zA-Z]/; - let error; - //checking for input error / setting error type - if (!contextInput || contextInput.trim() === '') { - error = 'empty'; - } else if (!contextInput.charAt(0).match(letters)) { - error = 'letters'; - } else if (!contextInput.match(/^[0-9a-zA-Z]+$/)) { - error = 'symbolsDetected'; - } else if ( - state.allContext.some( - (context) => context.name.toLowerCase() === contextInput.toLowerCase() - ) - ) { - error = 'dupe'; - } - - if (error !== undefined) { - triggerError(error); - return; - } - - dispatch(addContext({ name: contextInput })); - - if (roomCode) { - emitEvent('addContextAction', roomCode, { name: contextInput }); - } - - setContextInput(''); - }; - - const triggerError = (type: String) => { - setErrorStatus(true); - switch (type) { - case 'empty': - setErrorMsg('Context name cannot be blank.'); - break; - case 'dupe': - setErrorMsg('Context name already exists.'); - break; - case 'letters': - setErrorMsg('Context name must start with a letter.'); - break; - case 'symbolsDetected': - setErrorMsg('Context name must not contain symbols.'); - break; - } - }; - - //update data store when user add new key-value pair to context - const handleClickInputData = (name, { inputKey, inputValue }) => { - dispatch(addContextValues({ name, inputKey, inputValue })); - - if (roomCode) { - emitEvent('addContextValuesAction', roomCode, { - name, - inputKey, - inputValue - }); - } - }; - - //update data store when user deletes context - const handleDeleteContextClick = () => { - dispatch(deleteContext({ name: currentContext })); - - if (roomCode) { - emitEvent('deleteContextAction', roomCode, { name: currentContext }); - } - - setContextInput(''); - setCurrentContext(''); - }; - - return ( - <> - - - - - - - - - - - - - - - - - Context Data Table - - - - - - ); -}; - -export default CreateContainer; +import React from 'react'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import DataTable from './components/DataTable'; +import AddDataForm from './components/AddDataForm'; +import AddContextForm from './components/AddContextForm'; +import { Typography } from '@mui/material'; +import { + addContext, + deleteContext, + addContextValues +} from '../../../redux/reducers/slice/contextReducer'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../../redux/store'; +import { emitEvent } from '../../../../src/helperFunctions/socket'; + +const CreateContainer = () => { + const state = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const [contextInput, setContextInput] = React.useState(''); + const [currentContext, setCurrentContext] = React.useState(''); + const [errorMsg, setErrorMsg] = React.useState(''); + const [errorStatus, setErrorStatus] = React.useState(false); + const currentKeyValues = state.allContext.find( + (obj) => obj.name === currentContext + )?.values || [{ key: 'Enter Key', value: 'Enter value' }]; + const dispatch = useDispatch(); + + //update data store when user adds a new context + const handleClickSelectContext = () => { + let letters = /[a-zA-Z]/; + let error; + //checking for input error / setting error type + if (!contextInput || contextInput.trim() === '') { + error = 'empty'; + } else if (!contextInput.charAt(0).match(letters)) { + error = 'letters'; + } else if (!contextInput.match(/^[0-9a-zA-Z]+$/)) { + error = 'symbolsDetected'; + } else if ( + state.allContext.some( + (context) => context.name.toLowerCase() === contextInput.toLowerCase() + ) + ) { + error = 'dupe'; + } + + if (error !== undefined) { + triggerError(error); + return; + } + + dispatch(addContext({ name: contextInput })); + + if (roomCode) { + emitEvent('addContextAction', roomCode, { name: contextInput }); + } + + setContextInput(''); + }; + + const triggerError = (type: String) => { + setErrorStatus(true); + switch (type) { + case 'empty': + setErrorMsg('Context name cannot be blank.'); + break; + case 'dupe': + setErrorMsg('Context name already exists.'); + break; + case 'letters': + setErrorMsg('Context name must start with a letter.'); + break; + case 'symbolsDetected': + setErrorMsg('Context name must not contain symbols.'); + break; + } + }; + + //update data store when user add new key-value pair to context + const handleClickInputData = (name, { inputKey, inputValue }) => { + dispatch(addContextValues({ name, inputKey, inputValue })); + + if (roomCode) { + emitEvent('addContextValuesAction', roomCode, { + name, + inputKey, + inputValue + }); + } + }; + + //update data store when user deletes context + const handleDeleteContextClick = () => { + dispatch(deleteContext({ name: currentContext })); + + if (roomCode) { + emitEvent('deleteContextAction', roomCode, { name: currentContext }); + } + + setContextInput(''); + setCurrentContext(''); + }; + + return ( + <> + + + + + + + + + + + + + + + + + Context Data Table + + + + + + ); +}; + +export default CreateContainer; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx index beae68878..2cfb0a1f7 100644 --- a/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx @@ -1,163 +1,163 @@ -import React, { Fragment, useState } from 'react'; -import TextField from '@mui/material/TextField'; -import Select from '@mui/material/Select'; -import Snackbar from '@mui/material/Snackbar'; -import Button from '@mui/material/Button'; -import Box from '@mui/material/Box'; -import FormControl from '@mui/material/FormControl'; -import MuiAlert, { AlertProps } from '@mui/material/Alert'; -import { InputLabel, MenuItem, Typography } from '@mui/material'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../../../redux/store'; - -const AddContextForm = ({ - contextStore, - handleClickSelectContext, - handleDeleteContextClick, - contextInput, - setContextInput, - currentContext, - setCurrentContext, - errorMsg, - errorStatus, - setErrorStatus -}) => { - const { allContext } = contextStore; - const [btnDisabled, setBtnDisabled] = useState(false); - const [open, setOpen] = useState(false); - - const color = 'white'; - - //handler for submitting new context for creation - const handleSubmit = () => { - handleClickSelectContext(); - setOpen(true); - }; - - //form control for new context field - const handleChange = (e) => { - setErrorStatus(false); - setOpen(false); - setContextInput(e.target.value); - }; - - //event handle for confirmation modal - const handleClose = ( - event: React.SyntheticEvent | Event, - reason?: string - ) => { - if (reason === 'clickaway') { - return; - } - - setOpen(false); - }; - - const Alert = React.forwardRef(function Alert( - props, - ref - ) { - return ; - }); - - //creating options for context dropdown - const contexts = allContext.length ? ( - allContext.map((context, index) => { - return ( - - {context.name} - - ); - }) - ) : ( - No Contexts Created - ); - - return ( - - - Create Context - - - - - - Context Created - - - - - - Select Context - - - - select context - - - - - - ); -}; - -export default AddContextForm; +import React, { Fragment, useState } from 'react'; +import TextField from '@mui/material/TextField'; +import Select from '@mui/material/Select'; +import Snackbar from '@mui/material/Snackbar'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import FormControl from '@mui/material/FormControl'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import { InputLabel, MenuItem, Typography } from '@mui/material'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../../redux/store'; + +const AddContextForm = ({ + contextStore, + handleClickSelectContext, + handleDeleteContextClick, + contextInput, + setContextInput, + currentContext, + setCurrentContext, + errorMsg, + errorStatus, + setErrorStatus +}) => { + const { allContext } = contextStore; + const [btnDisabled, setBtnDisabled] = useState(false); + const [open, setOpen] = useState(false); + + const color = 'white'; + + //handler for submitting new context for creation + const handleSubmit = () => { + handleClickSelectContext(); + setOpen(true); + }; + + //form control for new context field + const handleChange = (e) => { + setErrorStatus(false); + setOpen(false); + setContextInput(e.target.value); + }; + + //event handle for confirmation modal + const handleClose = ( + event: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === 'clickaway') { + return; + } + + setOpen(false); + }; + + const Alert = React.forwardRef(function Alert( + props, + ref + ) { + return ; + }); + + //creating options for context dropdown + const contexts = allContext.length ? ( + allContext.map((context, index) => { + return ( + + {context.name} + + ); + }) + ) : ( + No Contexts Created + ); + + return ( + + + Create Context + + + + + + Context Created + + + + + + Select Context + + + + select context + + + + + + ); +}; + +export default AddContextForm; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx b/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx index de38a087f..a49dd8b35 100644 --- a/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/components/AddDataForm.tsx @@ -1,76 +1,76 @@ -import React from 'react'; -import TextField from '@mui/material/TextField'; -import Button from '@mui/material/Button'; -import Box from '@mui/material/Box'; -import { Typography } from '@mui/material'; -// import { useSelector } from 'react-redux'; -// import { RootState } from '../../../../redux/store'; - -const AddDataForm = ({ handleClickInputData, currentContext }) => { - //const [contextInput, setContextInput] = React.useState(null); - const defaultInputData = { inputKey: '', inputValue: '' }; - const [dataContext, setDataContext] = React.useState(defaultInputData); - - const saveData = () => { - setDataContext(defaultInputData); - if (dataContext.inputKey === '' || dataContext.inputValue === '') { - window.alert('empty key or value'); - return; - } - handleClickInputData(currentContext, dataContext); - }; - const color = 'white'; - - const handleChange = (e) => { - setDataContext((prevDataContext) => { - return { - ...prevDataContext, - [e.target.name]: e.target.value - }; - }); - }; - - return ( - <> - - Add Context Data - - - handleChange(e)} - InputProps={{ style: { color: color } }} - style={{ border: '1px solid black', width: '205px' }} - /> - handleChange(e)} - style={{ border: '1px solid black', width: '205px' }} - InputProps={{ style: { color: color } }} - /> - - - - ); -}; - -export default AddDataForm; +import React from 'react'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { Typography } from '@mui/material'; +// import { useSelector } from 'react-redux'; +// import { RootState } from '../../../../redux/store'; + +const AddDataForm = ({ handleClickInputData, currentContext }) => { + //const [contextInput, setContextInput] = React.useState(null); + const defaultInputData = { inputKey: '', inputValue: '' }; + const [dataContext, setDataContext] = React.useState(defaultInputData); + + const saveData = () => { + setDataContext(defaultInputData); + if (dataContext.inputKey === '' || dataContext.inputValue === '') { + window.alert('empty key or value'); + return; + } + handleClickInputData(currentContext, dataContext); + }; + const color = 'white'; + + const handleChange = (e) => { + setDataContext((prevDataContext) => { + return { + ...prevDataContext, + [e.target.name]: e.target.value + }; + }); + }; + + return ( + <> + + Add Context Data + + + handleChange(e)} + InputProps={{ style: { color: color } }} + style={{ border: '1px solid black', width: '205px' }} + /> + handleChange(e)} + style={{ border: '1px solid black', width: '205px' }} + InputProps={{ style: { color: color } }} + /> + + + + ); +}; + +export default AddDataForm; diff --git a/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx b/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx index 2f9bd24d8..838c24ece 100644 --- a/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/components/DataTable.tsx @@ -1,68 +1,68 @@ -import React from 'react'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import { styled } from '@mui/material/styles'; -import TableCell, { tableCellClasses } from '@mui/material/TableCell'; - -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: 'theme.palette.common.white' - }, - [`&.${tableCellClasses.body}`]: { - fontSize: 14 - } -})); - -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover - }, - // hide last border - '&:last-child td, &:last-child th': { - border: 0 - } -})); - -export default function DataTable({ target, currentContext }) { - return ( - <> - - - - - - {currentContext ? currentContext : 'Context Name'} - - - - - {target.map((data, index) => ( - - - {data.key} - - - {data.value} - - - ))} - -
    -
    - - ); -} +import React from 'react'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import { styled } from '@mui/material/styles'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: 'theme.palette.common.white' + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})); + +export default function DataTable({ target, currentContext }) { + return ( + <> + + + + + + {currentContext ? currentContext : 'Context Name'} + + + + + {target.map((data, index) => ( + + + {data.key} + + + {data.value} + + + ))} + +
    +
    + + ); +} diff --git a/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx b/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx index 3d5b59b19..7f115a3b1 100644 --- a/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx +++ b/app/src/components/ContextAPIManager/DisplayTab/DisplayContainer.tsx @@ -1,53 +1,53 @@ -import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { Chart } from 'react-google-charts'; -import Grid from '@mui/material/Grid'; -import { RootState } from '../../../redux/store'; - -const DisplayContainer = () => { - const allContext = useSelector( - (store: RootState) => store.contextSlice.allContext - ); - const [contextData, setContextData] = useState([]); - - useEffect(() => { - transformData(); - }, []); - - //formats context data for use in react google charts - const transformData = () => { - const formattedData = allContext - .map((obj) => { - return obj.components.map((component) => { - return [`App ⎯⎯ ${obj.name} ⎯⎯ ${component}`]; - }); - }) - .flat(); - setContextData([['Phrases'], ...formattedData]); - }; - - //format options for google chart - const options = { - wordtree: { - format: 'implicit', - word: 'App' - }, - backgroundColor: '#1E2024' - }; - - return ( - - {contextData.length < 2 &&

    No Contexts Consumed

    } - - - -
    - ); -}; -export default DisplayContainer; +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Chart } from 'react-google-charts'; +import Grid from '@mui/material/Grid'; +import { RootState } from '../../../redux/store'; + +const DisplayContainer = () => { + const allContext = useSelector( + (store: RootState) => store.contextSlice.allContext + ); + const [contextData, setContextData] = useState([]); + + useEffect(() => { + transformData(); + }, []); + + //formats context data for use in react google charts + const transformData = () => { + const formattedData = allContext + .map((obj) => { + return obj.components.map((component) => { + return [`App ⎯⎯ ${obj.name} ⎯⎯ ${component}`]; + }); + }) + .flat(); + setContextData([['Phrases'], ...formattedData]); + }; + + //format options for google chart + const options = { + wordtree: { + format: 'implicit', + word: 'App' + }, + backgroundColor: '#1E2024' + }; + + return ( + + {contextData.length < 2 &&

    No Contexts Consumed

    } + + + +
    + ); +}; +export default DisplayContainer; diff --git a/app/src/components/StateManagement/CreateTab/CreateContainer.tsx b/app/src/components/StateManagement/CreateTab/CreateContainer.tsx index cdd6b324f..08da0456c 100644 --- a/app/src/components/StateManagement/CreateTab/CreateContainer.tsx +++ b/app/src/components/StateManagement/CreateTab/CreateContainer.tsx @@ -1,14 +1,14 @@ -import React from 'react'; -import Grid from '@mui/material/Grid'; -import StatePropsPanel from './components/StatePropsPanel'; - -const CreateContainer = ({isThemeLight, data}) => { - - return ( - - - - ); -}; - -export default CreateContainer; +import React from 'react'; +import Grid from '@mui/material/Grid'; +import StatePropsPanel from './components/StatePropsPanel'; + +const CreateContainer = ({isThemeLight, data}) => { + + return ( + + + + ); +}; + +export default CreateContainer; diff --git a/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx b/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx index f43c57d31..7f455e05e 100644 --- a/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx +++ b/app/src/components/StateManagement/CreateTab/components/StatePropsPanel.tsx @@ -1,621 +1,621 @@ -import React, { useState, useEffect } from 'react'; -import { Theme } from '@mui/material/styles'; -import makeStyles from '@mui/styles/makeStyles'; -import { useDispatch, useSelector } from 'react-redux'; -import { addState } from '../../../../redux/reducers/slice/appStateSlice'; -import { - FormControl, - FormHelperText, - MenuItem, - InputLabel, - Select, - TextField, - Button -} from '@mui/material'; -import TableStateProps from './TableStateProps'; -import TableParentProps from './TableParentProps'; -import TablePassedInProps from './TablePassedInProps'; -import { RootState } from '../../../../redux/store'; -import { emitEvent } from '../../../../helperFunctions/socket'; - -const StatePropsPanel = ({ isThemeLight, data }): JSX.Element => { - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - const classes = useStyles(); - const [inputKey, setInputKey] = useState(''); - const [inputValue, setInputValue] = useState(''); - const [inputType, setInputType] = useState(''); - const [errorStatus, setErrorStatus] = useState(false); - const [inputTypeError, setInputTypeError] = useState(''); - const [newVal, setNewVal] = useState('test'); - const [errorMsg, setErrorMsg] = useState(''); - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - const [parentProps, setParentProps] = useState([]); - const [parentPassedInProps, setParentPassedInProps] = useState([]); - const [parentName, setParentName] = useState('No Parents'); - const [parentComponent, setParentComponent] = useState({}); - const [rows1, setRows1] = useState(currentComponent.stateProps); - const [propNum, setPropNum] = useState(1); - - // convert value to correct type based on user input - const typeConversion = (value: string, type: string) => { - switch (type) { - case 'string': { - setInputTypeError(''); - return String(value); - } - case 'number': { - setInputTypeError(''); - return Number(value); - } - case 'boolean': { - setInputTypeError(''); - return value === 'true'; - } - case 'array': - try { - let retVal = JSON.parse(value); - if (Array.isArray(retVal)) { - setInputTypeError(''); - return retVal; - } else { - throw new Error('Input was not an array!'); - } - } catch { - setInputTypeError(type); - return null; - } - case 'object': { - try { - let retVal = JSON.parse(value); - - if (typeof retVal === 'object' && !Array.isArray(retVal)) { - setInputTypeError(''); - return retVal; - } else { - throw new Error('Input was not an object (excluding Arrays)!'); - } - } catch { - setInputTypeError(type); - return null; - } - } - default: { - setInputTypeError(''); - return value; - } - } - }; - - // clears the input key, value, and type on Form - const clearForm = () => { - setInputKey(''); - setInputValue(''); - setInputType(''); - }; - - useEffect(() => { - setNewVal(typeConversion(inputValue, inputType)); - }, [inputType, inputValue]); - - // submit new stateProps entries to state context - const submitNewState = (e) => { - e.preventDefault(); - - // don't allow them to submit state without all fields - if (!inputKey || !inputType || !inputValue) { - setErrorStatus(true); - setErrorMsg('All fields are required'); - return; - } - - const statesArray = currentComponent.stateProps; - //loop though array, access each obj at key property - let keyToInt = parseInt(inputKey[0]); - if (!isNaN(keyToInt)) { - setErrorStatus(true); - setErrorMsg('Key name can not start with int.'); - return; - } - - // check here to see if state has already been created with the submitted key - for (let i = 0; i < state.components.length; i++) { - for (let j = 0; j < state.components[i].stateProps.length; j++) { - if (inputKey === state.components[i].stateProps[j]['key']) { - setErrorStatus(true); - setErrorMsg('Key name already in use.'); - return; - } else { - setErrorStatus(false); - setErrorMsg(''); - } - } - } - setPropNum((prev) => prev + 1); - const newState = { - // id name of state will be the parent component name concated with propNum. it will start at 1 and increase by 1 for each new state added - id: `${currentComponent.name}-${inputKey}`, - key: inputKey, - value: newVal, - type: inputType - }; - - const setNewState = { - // id name of state will be the parent component name concated with propNum. it will start at 1 and increase by 1 for each new state added - id: `${currentComponent.name}-set${inputKey - .slice(0, 1) - .toUpperCase()}${inputKey.slice(1)}`, - key: `set${inputKey.slice(0, 1).toUpperCase()}${inputKey.slice(1)}`, - value: '', - type: 'func' - }; - if (!inputTypeError) { - dispatch( - addState({ - newState: newState, - setNewState: setNewState, - contextParam: contextParam - }) - ); - - if (roomCode) { - emitEvent('addStateAction', roomCode, { - newState: newState, - setNewState: setNewState, - contextParam: contextParam - }); - } - - setRows1([...rows1, newState]); - setErrorStatus(false); - clearForm(); - } - }; - - // find table row using its id and if it exists, populate form with its details - const handlerRowSelect = (table) => { - let exists = false; - currentComponent.stateProps.forEach((stateProp) => { - // if stateProp id matches current row's id (table.row.id), flip exists to true - if (stateProp.id === table.row.id) exists = true; - }); - // if row id exists, populate form with corresponding inputs (key, value, type) from table row - if (exists) { - setInputKey(table.row.key); - setInputType(table.row.type); - setInputValue(table.row.value ? JSON.stringify(table.row.value) : ''); - } else clearForm(); - }; - //use effect to populate parent props table on load and every time canvas focus changes - useEffect(() => { - const parentInfo = findParent(currentId); - - setParentProps(parentInfo.parentProps); - setParentName(parentInfo.parentName); - setParentComponent(parentInfo.parentComponent); - setParentPassedInProps(parentInfo.parentPassedInProps); - }, [currentId]); - - const findParent = (childId) => { - let arr = []; - - for (let i = 0; i < data.length; i++) { - let currComponent = data[i]; - for (let j = 0; j < currComponent.children.length; j++) { - let currChild = currComponent.children[j]; - if (currChild.typeId === childId) { - const currComponentCopy = JSON.parse(JSON.stringify(currComponent)); - - return { - parentProps: currComponentCopy.stateProps, - parentName: currComponentCopy.name, - parentComponent: currComponentCopy, - parentPassedInProps: currComponentCopy.passedInProps - }; - } - } - } - return { parentProps: [], parentName: '' }; - }; - - return ( -
    -
    - -

    - Create New State -

    - setInputKey(e.target.value)} - helperText={errorStatus ? errorMsg : ''} - className={ - isThemeLight - ? `${classes.rootLight} ${classes.inputTextLight}` - : `${classes.rootDark} ${classes.inputTextDark}` - } - /> - setInputValue(e.target.value)} - className={ - isThemeLight - ? `${classes.rootLight} ${classes.inputTextLight}` - : `${classes.rootDark} ${classes.inputTextDark}` - } - /> - - - type - - - - {inputTypeError === 'object' - ? 'JSON object form: {"key": value}' - : inputTypeError === 'array' - ? 'Array form: [value]' - : 'Required'} - - -
    - -
    -
    -
    -
    -
    -
    -

    - Current Component State:{' '} - {state.components[state.canvasFocus.componentId - 1].name} -

    - -
    - -
    -

    - Available Props from Parent:{' '} - {parentName ? parentName : 'No Parents'} -

    - -
    - -
    - - - -
    - -
    -

    - Passed in Props from Parent:{' '} - {parentName ? parentName : 'No Parents'} -

    - -
    -
    -
    - ); -}; -const useStyles = makeStyles((theme: Theme) => ({ - inputField: { - marginTop: '10px', - borderRadius: '5px', - whiteSpace: 'nowrap', - overflowX: 'hidden', - textOverflow: 'ellipsis', - backgroundColor: 'rgba(255,255,255,0.15)', - margin: '0px 0px 0px 10px', - width: '140px', - height: '30px', - borderColor: 'white' - }, - inputWrapper: { - textAlign: 'center', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: '15px' - }, - addComponentWrapper: { - padding: 'auto', - marginLeft: '21px', - display: 'inline-block', - width: '100%' - }, - rootCheckBox: { - borderColor: '#0671e3', - padding: '0px' - }, - rootCheckBoxLabel: { - borderColor: '#0671e3' - }, - panelWrapper: { - width: '100%', - marginTop: '15px', - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }, - panelWrapperList: { - minHeight: '120px', - marginLeft: '-15px', - marginRight: '-15px', - width: '300px', - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }, - dragComponents: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - textAlign: 'center', - width: '500px', - backgroundColor: '#0671e3', - border: '5px solid #0671e3' - }, - panelSubheader: { - textAlign: 'center', - color: '#fff' - }, - input: {}, - newComponent: { - color: '#3c59ba', - fontSize: '95%', - marginBottom: '20px' - }, - inputLabel: { - fontSize: '1em', - marginLeft: '10px' - }, - btnGroup: { - display: 'flex', - flexDirection: 'column' - }, - addComponentButton: { - height: '42px', - width: '100px', - fontFamily: 'Roboto, Raleway, sans-serif', - fontSize: '14.5px', - textAlign: 'center', - margin: '-20px 0px 5px 150px', - border: ' 1px solid #0671E3', - borderRadius: '8px', - transition: '0.3s' - }, - rootToggle: { - color: '#696969', - fontSize: '0.85rem' - }, - lightThemeFontColor: { - color: 'white' - }, - darkThemeFontColor: { - color: '#fff' - }, - greyThemeFontColor: { - color: 'white' - }, - formControl: { - margin: '8px', - minWidth: 120 - }, - selectEmpty: { - marginTop: '16px' - }, - color: { - color: '#fff' - }, - rootLight: { - '& .MuiFormLabel-root': { - color: 'white' - }, - margin: '5px' - }, - rootDark: { - '& .MuiFormLabel-root': {}, - '& .MuiOutlinedInput-notchedOutline': {}, - margin: '5px' - }, - underlineDark: { - borderBottom: '1px solid white' - }, - rootUnderlineDark: { - '& .-icon': { - color: '#fff' - }, - '&::before': { - borderBottom: '1px solid #fff' - } - }, - rootUnderlineLight: { - '& .-icon': { - color: 'rgba(0,0,0,0.54)' - }, - '&::before': { - borderBottom: '1px solid rgba(0,0,0,0.54)' - } - }, - inputTextDark: { - '& .MuiInputBase-input': { - color: 'white' - } - }, - inputTextLight: { - '& .MuiInputBase-input': { - color: 'white' - } - } -})); - -export default StatePropsPanel; +import React, { useState, useEffect } from 'react'; +import { Theme } from '@mui/material/styles'; +import makeStyles from '@mui/styles/makeStyles'; +import { useDispatch, useSelector } from 'react-redux'; +import { addState } from '../../../../redux/reducers/slice/appStateSlice'; +import { + FormControl, + FormHelperText, + MenuItem, + InputLabel, + Select, + TextField, + Button +} from '@mui/material'; +import TableStateProps from './TableStateProps'; +import TableParentProps from './TableParentProps'; +import TablePassedInProps from './TablePassedInProps'; +import { RootState } from '../../../../redux/store'; +import { emitEvent } from '../../../../helperFunctions/socket'; + +const StatePropsPanel = ({ isThemeLight, data }): JSX.Element => { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + const classes = useStyles(); + const [inputKey, setInputKey] = useState(''); + const [inputValue, setInputValue] = useState(''); + const [inputType, setInputType] = useState(''); + const [errorStatus, setErrorStatus] = useState(false); + const [inputTypeError, setInputTypeError] = useState(''); + const [newVal, setNewVal] = useState('test'); + const [errorMsg, setErrorMsg] = useState(''); + const currentId = state.canvasFocus.componentId; + const currentComponent = state.components[currentId - 1]; + const [parentProps, setParentProps] = useState([]); + const [parentPassedInProps, setParentPassedInProps] = useState([]); + const [parentName, setParentName] = useState('No Parents'); + const [parentComponent, setParentComponent] = useState({}); + const [rows1, setRows1] = useState(currentComponent.stateProps); + const [propNum, setPropNum] = useState(1); + + // convert value to correct type based on user input + const typeConversion = (value: string, type: string) => { + switch (type) { + case 'string': { + setInputTypeError(''); + return String(value); + } + case 'number': { + setInputTypeError(''); + return Number(value); + } + case 'boolean': { + setInputTypeError(''); + return value === 'true'; + } + case 'array': + try { + let retVal = JSON.parse(value); + if (Array.isArray(retVal)) { + setInputTypeError(''); + return retVal; + } else { + throw new Error('Input was not an array!'); + } + } catch { + setInputTypeError(type); + return null; + } + case 'object': { + try { + let retVal = JSON.parse(value); + + if (typeof retVal === 'object' && !Array.isArray(retVal)) { + setInputTypeError(''); + return retVal; + } else { + throw new Error('Input was not an object (excluding Arrays)!'); + } + } catch { + setInputTypeError(type); + return null; + } + } + default: { + setInputTypeError(''); + return value; + } + } + }; + + // clears the input key, value, and type on Form + const clearForm = () => { + setInputKey(''); + setInputValue(''); + setInputType(''); + }; + + useEffect(() => { + setNewVal(typeConversion(inputValue, inputType)); + }, [inputType, inputValue]); + + // submit new stateProps entries to state context + const submitNewState = (e) => { + e.preventDefault(); + + // don't allow them to submit state without all fields + if (!inputKey || !inputType || !inputValue) { + setErrorStatus(true); + setErrorMsg('All fields are required'); + return; + } + + const statesArray = currentComponent.stateProps; + //loop though array, access each obj at key property + let keyToInt = parseInt(inputKey[0]); + if (!isNaN(keyToInt)) { + setErrorStatus(true); + setErrorMsg('Key name can not start with int.'); + return; + } + + // check here to see if state has already been created with the submitted key + for (let i = 0; i < state.components.length; i++) { + for (let j = 0; j < state.components[i].stateProps.length; j++) { + if (inputKey === state.components[i].stateProps[j]['key']) { + setErrorStatus(true); + setErrorMsg('Key name already in use.'); + return; + } else { + setErrorStatus(false); + setErrorMsg(''); + } + } + } + setPropNum((prev) => prev + 1); + const newState = { + // id name of state will be the parent component name concated with propNum. it will start at 1 and increase by 1 for each new state added + id: `${currentComponent.name}-${inputKey}`, + key: inputKey, + value: newVal, + type: inputType + }; + + const setNewState = { + // id name of state will be the parent component name concated with propNum. it will start at 1 and increase by 1 for each new state added + id: `${currentComponent.name}-set${inputKey + .slice(0, 1) + .toUpperCase()}${inputKey.slice(1)}`, + key: `set${inputKey.slice(0, 1).toUpperCase()}${inputKey.slice(1)}`, + value: '', + type: 'func' + }; + if (!inputTypeError) { + dispatch( + addState({ + newState: newState, + setNewState: setNewState, + contextParam: contextParam + }) + ); + + if (roomCode) { + emitEvent('addStateAction', roomCode, { + newState: newState, + setNewState: setNewState, + contextParam: contextParam + }); + } + + setRows1([...rows1, newState]); + setErrorStatus(false); + clearForm(); + } + }; + + // find table row using its id and if it exists, populate form with its details + const handlerRowSelect = (table) => { + let exists = false; + currentComponent.stateProps.forEach((stateProp) => { + // if stateProp id matches current row's id (table.row.id), flip exists to true + if (stateProp.id === table.row.id) exists = true; + }); + // if row id exists, populate form with corresponding inputs (key, value, type) from table row + if (exists) { + setInputKey(table.row.key); + setInputType(table.row.type); + setInputValue(table.row.value ? JSON.stringify(table.row.value) : ''); + } else clearForm(); + }; + //use effect to populate parent props table on load and every time canvas focus changes + useEffect(() => { + const parentInfo = findParent(currentId); + + setParentProps(parentInfo.parentProps); + setParentName(parentInfo.parentName); + setParentComponent(parentInfo.parentComponent); + setParentPassedInProps(parentInfo.parentPassedInProps); + }, [currentId]); + + const findParent = (childId) => { + let arr = []; + + for (let i = 0; i < data.length; i++) { + let currComponent = data[i]; + for (let j = 0; j < currComponent.children.length; j++) { + let currChild = currComponent.children[j]; + if (currChild.typeId === childId) { + const currComponentCopy = JSON.parse(JSON.stringify(currComponent)); + + return { + parentProps: currComponentCopy.stateProps, + parentName: currComponentCopy.name, + parentComponent: currComponentCopy, + parentPassedInProps: currComponentCopy.passedInProps + }; + } + } + } + return { parentProps: [], parentName: '' }; + }; + + return ( +
    +
    + +

    + Create New State +

    + setInputKey(e.target.value)} + helperText={errorStatus ? errorMsg : ''} + className={ + isThemeLight + ? `${classes.rootLight} ${classes.inputTextLight}` + : `${classes.rootDark} ${classes.inputTextDark}` + } + /> + setInputValue(e.target.value)} + className={ + isThemeLight + ? `${classes.rootLight} ${classes.inputTextLight}` + : `${classes.rootDark} ${classes.inputTextDark}` + } + /> + + + type + + + + {inputTypeError === 'object' + ? 'JSON object form: {"key": value}' + : inputTypeError === 'array' + ? 'Array form: [value]' + : 'Required'} + + +
    + +
    +
    +
    +
    +
    +
    +

    + Current Component State:{' '} + {state.components[state.canvasFocus.componentId - 1].name} +

    + +
    + +
    +

    + Available Props from Parent:{' '} + {parentName ? parentName : 'No Parents'} +

    + +
    + +
    + + + +
    + +
    +

    + Passed in Props from Parent:{' '} + {parentName ? parentName : 'No Parents'} +

    + +
    +
    +
    + ); +}; +const useStyles = makeStyles((theme: Theme) => ({ + inputField: { + marginTop: '10px', + borderRadius: '5px', + whiteSpace: 'nowrap', + overflowX: 'hidden', + textOverflow: 'ellipsis', + backgroundColor: 'rgba(255,255,255,0.15)', + margin: '0px 0px 0px 10px', + width: '140px', + height: '30px', + borderColor: 'white' + }, + inputWrapper: { + textAlign: 'center', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: '15px' + }, + addComponentWrapper: { + padding: 'auto', + marginLeft: '21px', + display: 'inline-block', + width: '100%' + }, + rootCheckBox: { + borderColor: '#0671e3', + padding: '0px' + }, + rootCheckBoxLabel: { + borderColor: '#0671e3' + }, + panelWrapper: { + width: '100%', + marginTop: '15px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + panelWrapperList: { + minHeight: '120px', + marginLeft: '-15px', + marginRight: '-15px', + width: '300px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + dragComponents: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center', + width: '500px', + backgroundColor: '#0671e3', + border: '5px solid #0671e3' + }, + panelSubheader: { + textAlign: 'center', + color: '#fff' + }, + input: {}, + newComponent: { + color: '#3c59ba', + fontSize: '95%', + marginBottom: '20px' + }, + inputLabel: { + fontSize: '1em', + marginLeft: '10px' + }, + btnGroup: { + display: 'flex', + flexDirection: 'column' + }, + addComponentButton: { + height: '42px', + width: '100px', + fontFamily: 'Roboto, Raleway, sans-serif', + fontSize: '14.5px', + textAlign: 'center', + margin: '-20px 0px 5px 150px', + border: ' 1px solid #0671E3', + borderRadius: '8px', + transition: '0.3s' + }, + rootToggle: { + color: '#696969', + fontSize: '0.85rem' + }, + lightThemeFontColor: { + color: 'white' + }, + darkThemeFontColor: { + color: '#fff' + }, + greyThemeFontColor: { + color: 'white' + }, + formControl: { + margin: '8px', + minWidth: 120 + }, + selectEmpty: { + marginTop: '16px' + }, + color: { + color: '#fff' + }, + rootLight: { + '& .MuiFormLabel-root': { + color: 'white' + }, + margin: '5px' + }, + rootDark: { + '& .MuiFormLabel-root': {}, + '& .MuiOutlinedInput-notchedOutline': {}, + margin: '5px' + }, + underlineDark: { + borderBottom: '1px solid white' + }, + rootUnderlineDark: { + '& .-icon': { + color: '#fff' + }, + '&::before': { + borderBottom: '1px solid #fff' + } + }, + rootUnderlineLight: { + '& .-icon': { + color: 'rgba(0,0,0,0.54)' + }, + '&::before': { + borderBottom: '1px solid rgba(0,0,0,0.54)' + } + }, + inputTextDark: { + '& .MuiInputBase-input': { + color: 'white' + } + }, + inputTextLight: { + '& .MuiInputBase-input': { + color: 'white' + } + } +})); + +export default StatePropsPanel; diff --git a/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx b/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx index cf8a1567f..8a155b122 100644 --- a/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx +++ b/app/src/components/StateManagement/CreateTab/components/TableParentProps.tsx @@ -1,164 +1,164 @@ -import React, { useState, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { DataGrid, GridEditRowsModel } from '@mui/x-data-grid'; -import Button from '@mui/material/Button'; -import makeStyles from '@mui/styles/makeStyles'; -import AddIcon from '@mui/icons-material/Add'; -import { addPassedInProps } from '../../../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../../../redux/store'; -import { emitEvent } from '../../../../helperFunctions/socket'; - -const TableParentProps = (props) => { - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - const classes = useStyles(); - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - const [editRowsModel] = useState({}); - const [gridColumns, setGridColumns] = useState([]); - const parentProps = props.parentProps; - const parentPassedInProps = props.parentPassedInProps; - const parentComponent = props.parentComponent; - const columnTabs = [ - { - field: 'id', - headerName: 'ID', - width: 30, - editable: false - }, - { - field: 'key', - headerName: 'Key', - width: 90, - editable: true - }, - { - field: 'value', - headerName: 'Initial Value', - width: 100, - editable: true - }, - { - field: 'type', - headerName: 'Type', - width: 90, - editable: false - }, - { - field: 'delete', - headerName: '+', - width: 30, - editable: false, - renderCell: function renderCell(params: any) { - return ( - - ); - } - } - ]; - const addProps = (parentComponentProps, rowId) => { - // get the current focused component - // remove the state that the button is clicked - // send a dispatch to rerender the table - dispatch( - addPassedInProps({ - passedInProps: parentComponentProps, - rowId: rowId, - parentComponent: parentComponent, - contextParam: contextParam - }) - ); - - if (roomCode) { - emitEvent('addPassedInPropsAction', roomCode, { - passedInProps: parentComponentProps, - rowId: rowId, - parentComponent: parentComponent, - contextParam: contextParam - }); - } - }; - - useEffect(() => { - setGridColumns(columnTabs); - }, [props.isThemeLight]); - - // determine whether or not to include delete column in data grid - useEffect(() => { - if (props.canDeleteState) { - setGridColumns(columnTabs); - } else { - setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); - } - }, [state.canvasFocus.componentId]); - - let rows; - - // check if current component is a root component-- if yes, it shouldn't have any parent props - if (currentComponent.name === 'App' || currentComponent.name === 'index') { - rows = []; - } else { - if (parentProps) { - rows = parentProps; - if (parentPassedInProps) { - rows = [...rows, ...parentPassedInProps]; - } - } - } - - return ( -
    - -
    - ); -}; - -const useStyles = makeStyles({ - themeLight: { - color: 'white', - '& button:hover': { - backgroundColor: 'LightGray' - }, - '& button': { - color: 'white' - }, - '& .MuiTablePagination-root': { - color: 'white' - } - }, - themeDark: { - color: 'white', - '& .MuiTablePagination-root': { - color: 'white' - }, - '& .MuiIconButton-root': { - color: 'white' - }, - '& .MuiSvgIcon-root': { - color: 'white' - }, - '& .MuiDataGrid-window': { - backgroundColor: 'rgba(0,0,0,0.54)' - } - } -}); - -export default TableParentProps; +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { DataGrid, GridEditRowsModel } from '@mui/x-data-grid'; +import Button from '@mui/material/Button'; +import makeStyles from '@mui/styles/makeStyles'; +import AddIcon from '@mui/icons-material/Add'; +import { addPassedInProps } from '../../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../../redux/store'; +import { emitEvent } from '../../../../helperFunctions/socket'; + +const TableParentProps = (props) => { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + const classes = useStyles(); + const currentId = state.canvasFocus.componentId; + const currentComponent = state.components[currentId - 1]; + const [editRowsModel] = useState({}); + const [gridColumns, setGridColumns] = useState([]); + const parentProps = props.parentProps; + const parentPassedInProps = props.parentPassedInProps; + const parentComponent = props.parentComponent; + const columnTabs = [ + { + field: 'id', + headerName: 'ID', + width: 30, + editable: false + }, + { + field: 'key', + headerName: 'Key', + width: 90, + editable: true + }, + { + field: 'value', + headerName: 'Initial Value', + width: 100, + editable: true + }, + { + field: 'type', + headerName: 'Type', + width: 90, + editable: false + }, + { + field: 'delete', + headerName: '+', + width: 30, + editable: false, + renderCell: function renderCell(params: any) { + return ( + + ); + } + } + ]; + const addProps = (parentComponentProps, rowId) => { + // get the current focused component + // remove the state that the button is clicked + // send a dispatch to rerender the table + dispatch( + addPassedInProps({ + passedInProps: parentComponentProps, + rowId: rowId, + parentComponent: parentComponent, + contextParam: contextParam + }) + ); + + if (roomCode) { + emitEvent('addPassedInPropsAction', roomCode, { + passedInProps: parentComponentProps, + rowId: rowId, + parentComponent: parentComponent, + contextParam: contextParam + }); + } + }; + + useEffect(() => { + setGridColumns(columnTabs); + }, [props.isThemeLight]); + + // determine whether or not to include delete column in data grid + useEffect(() => { + if (props.canDeleteState) { + setGridColumns(columnTabs); + } else { + setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); + } + }, [state.canvasFocus.componentId]); + + let rows; + + // check if current component is a root component-- if yes, it shouldn't have any parent props + if (currentComponent.name === 'App' || currentComponent.name === 'index') { + rows = []; + } else { + if (parentProps) { + rows = parentProps; + if (parentPassedInProps) { + rows = [...rows, ...parentPassedInProps]; + } + } + } + + return ( +
    + +
    + ); +}; + +const useStyles = makeStyles({ + themeLight: { + color: 'white', + '& button:hover': { + backgroundColor: 'LightGray' + }, + '& button': { + color: 'white' + }, + '& .MuiTablePagination-root': { + color: 'white' + } + }, + themeDark: { + color: 'white', + '& .MuiTablePagination-root': { + color: 'white' + }, + '& .MuiIconButton-root': { + color: 'white' + }, + '& .MuiSvgIcon-root': { + color: 'white' + }, + '& .MuiDataGrid-window': { + backgroundColor: 'rgba(0,0,0,0.54)' + } + } +}); + +export default TableParentProps; diff --git a/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx b/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx index 46d1db99a..1dfc45dcd 100644 --- a/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx +++ b/app/src/components/StateManagement/CreateTab/components/TablePassedInProps.tsx @@ -1,150 +1,150 @@ -import React, { useState, useEffect } from 'react'; -import { DataGrid, GridEditRowsModel } from '@mui/x-data-grid'; -import Button from '@mui/material/Button'; -import ClearIcon from '@mui/icons-material/Clear'; -import makeStyles from '@mui/styles/makeStyles'; -import { useDispatch, useSelector } from 'react-redux'; -import { deletePassedInProps } from '../../../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../../../redux/store'; -import { ColumnTab } from '../../../../interfaces/Interfaces'; -import { emitEvent } from '../../../../helperFunctions/socket'; - -const TablePassedInProps = (props) => { - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - const classes = useStyles(); - const [editRowsModel] = useState({}); - const [gridColumns, setGridColumns] = useState([]); - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - const passedInProps = - currentComponent.name !== 'App' && currentComponent.name !== 'index' - ? currentComponent.passedInProps - : []; - - //formatting for data grid columns - const columnTabs: ColumnTab[] = [ - { - field: 'id', - headerName: 'ID', - width: 30, - editable: false - }, - { - field: 'key', - headerName: 'Key', - width: 90, - editable: true - }, - { - field: 'value', - headerName: 'Initial Value', - width: 100, - editable: true - }, - { - field: 'type', - headerName: 'Type', - width: 90, - editable: false - }, - { - field: 'delete', - headerName: 'X', - width: 30, - editable: false, - align: 'left', - renderCell: function renderCell(params: any) { - return ( - - ); - } - } - ]; - - const deleteProps = (rowId) => { - // get the current focused component - // remove the state that the button is clicked - // send a dispatch to rerender the table - dispatch(deletePassedInProps({ rowId: rowId, contextParam: contextParam })); - - if (roomCode) { - emitEvent('deletePassedInPropsAction', roomCode, { - rowId: rowId, - contextParam: contextParam - }); - } - }; - - useEffect(() => { - setGridColumns(columnTabs); - }, [props.isThemeLight]); - - useEffect(() => { - if (props.canDeleteState) { - setGridColumns(columnTabs); - } else { - setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); - } - }, [state.canvasFocus.componentId]); - - // fill data grid rows with all of the passed in props from parent component (if there are any) - let rows: any = passedInProps?.slice(); - //let rows: readonly StateProp[] = passedInProps?.slice() || []; - - return ( -
    - -
    - ); -}; -// colors of state mgmt modal -const useStyles = makeStyles({ - themeLight: { - color: 'white', - '& button:hover': { - backgroundColor: 'LightGray' - }, - '& button': { - color: 'white' - }, - '& .MuiTablePagination-root': { - color: 'rbga(0,0,0,0.54)' - } - }, - themeDark: { - color: 'white', - '& .MuiTablePagination-root': { - color: 'white' - }, - '& .MuiIconButton-root': { - color: 'white' - }, - '& .MuiSvgIcon-root': { - color: 'white' - }, - '& .MuiDataGrid-window': { - backgroundColor: 'rgba(0,0,0,0.54)' - } - } -}); - -export default TablePassedInProps; +import React, { useState, useEffect } from 'react'; +import { DataGrid, GridEditRowsModel } from '@mui/x-data-grid'; +import Button from '@mui/material/Button'; +import ClearIcon from '@mui/icons-material/Clear'; +import makeStyles from '@mui/styles/makeStyles'; +import { useDispatch, useSelector } from 'react-redux'; +import { deletePassedInProps } from '../../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../../redux/store'; +import { ColumnTab } from '../../../../interfaces/Interfaces'; +import { emitEvent } from '../../../../helperFunctions/socket'; + +const TablePassedInProps = (props) => { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + const classes = useStyles(); + const [editRowsModel] = useState({}); + const [gridColumns, setGridColumns] = useState([]); + const currentId = state.canvasFocus.componentId; + const currentComponent = state.components[currentId - 1]; + const passedInProps = + currentComponent.name !== 'App' && currentComponent.name !== 'index' + ? currentComponent.passedInProps + : []; + + //formatting for data grid columns + const columnTabs: ColumnTab[] = [ + { + field: 'id', + headerName: 'ID', + width: 30, + editable: false + }, + { + field: 'key', + headerName: 'Key', + width: 90, + editable: true + }, + { + field: 'value', + headerName: 'Initial Value', + width: 100, + editable: true + }, + { + field: 'type', + headerName: 'Type', + width: 90, + editable: false + }, + { + field: 'delete', + headerName: 'X', + width: 30, + editable: false, + align: 'left', + renderCell: function renderCell(params: any) { + return ( + + ); + } + } + ]; + + const deleteProps = (rowId) => { + // get the current focused component + // remove the state that the button is clicked + // send a dispatch to rerender the table + dispatch(deletePassedInProps({ rowId: rowId, contextParam: contextParam })); + + if (roomCode) { + emitEvent('deletePassedInPropsAction', roomCode, { + rowId: rowId, + contextParam: contextParam + }); + } + }; + + useEffect(() => { + setGridColumns(columnTabs); + }, [props.isThemeLight]); + + useEffect(() => { + if (props.canDeleteState) { + setGridColumns(columnTabs); + } else { + setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); + } + }, [state.canvasFocus.componentId]); + + // fill data grid rows with all of the passed in props from parent component (if there are any) + let rows: any = passedInProps?.slice(); + //let rows: readonly StateProp[] = passedInProps?.slice() || []; + + return ( +
    + +
    + ); +}; +// colors of state mgmt modal +const useStyles = makeStyles({ + themeLight: { + color: 'white', + '& button:hover': { + backgroundColor: 'LightGray' + }, + '& button': { + color: 'white' + }, + '& .MuiTablePagination-root': { + color: 'rbga(0,0,0,0.54)' + } + }, + themeDark: { + color: 'white', + '& .MuiTablePagination-root': { + color: 'white' + }, + '& .MuiIconButton-root': { + color: 'white' + }, + '& .MuiSvgIcon-root': { + color: 'white' + }, + '& .MuiDataGrid-window': { + backgroundColor: 'rgba(0,0,0,0.54)' + } + } +}); + +export default TablePassedInProps; diff --git a/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx b/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx index 26f8666e2..033b3d727 100644 --- a/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx +++ b/app/src/components/StateManagement/CreateTab/components/TableStateProps.tsx @@ -1,163 +1,163 @@ -import React, { useState, useEffect } from 'react'; -import { DataGrid, GridEditRowsModel } from '@mui/x-data-grid'; -import Button from '@mui/material/Button'; -import ClearIcon from '@mui/icons-material/Clear'; -import makeStyles from '@mui/styles/makeStyles'; -import { StatePropsPanelProps } from '../../../../interfaces/Interfaces'; -import { useDispatch, useSelector } from 'react-redux'; -import { deleteState } from '../../../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../../../redux/store'; -import { ColumnTab } from '../../../../interfaces/Interfaces'; -import { emitEvent } from '../../../../helperFunctions/socket'; - -// updates state mgmt boxes and data grid -const TableStateProps = (props) => { - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - const classes = useStyles(); - const [editRowsModel] = useState({}); - const [gridColumns, setGridColumns] = useState([]); - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - - // formatting for data grid columns - const columnTabs: ColumnTab[] = [ - { - field: 'id', - headerName: 'ID', - width: 30, - editable: false - }, - { - field: 'key', - headerName: 'Key', - width: 90, - editable: true - }, - { - field: 'value', - headerName: 'Initial Value', - width: 100, - editable: true, - valueGetter: (param) => { - //to display the actual object or array instead of [object Object], leave undefined if it is setter function - if (param.row.type === 'func') return; - return JSON.stringify(param.row.value); - } - }, - { - field: 'type', - headerName: 'Type', - width: 90, - editable: false - }, - { - field: 'delete', - headerName: 'X', - width: 30, - editable: false, - renderCell: function renderCell(params: any) { - return ( - - ); - } - } - ]; - - const handleDeleteState = (selectedId) => { - const currentId = state.canvasFocus.componentId; - const currentComponent = state.components[currentId - 1]; - const filtered = currentComponent.stateProps.filter( - (element) => element.id !== selectedId - ); - dispatch( - deleteState({ - stateProps: filtered, - rowId: selectedId, - contextParam: contextParam - }) - ); - - if (roomCode) { - emitEvent('deleteStateAction', roomCode, { - stateProps: filtered, - rowId: selectedId, - contextParam: contextParam - }); - } - }; - - useEffect(() => { - setGridColumns(columnTabs); - }, [props.isThemeLight]); - - const { selectHandler }: StatePropsPanelProps = props; - - useEffect(() => { - if (props.canDeleteState) { - setGridColumns(columnTabs); - } else { - setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); - } - }, [state.canvasFocus.componentId]); - - // rows to show are either from current component or from a given provider - let rows = []; - currentComponent.stateProps?.forEach((prop) => { - rows.push(prop); - }); - - return ( -
    - -
    - ); -}; - -const useStyles = makeStyles({ - themeLight: { - color: 'white', - '& button:hover': { - backgroundColor: 'LightGray' - }, - '& button': { - color: 'white' - } - }, - themeDark: { - color: 'white', - '& .MuiTablePagination-root': { - color: 'white' - }, - '& .MuiIconButton-root': { - color: 'white' - }, - '& .MuiSvgIcon-root': { - color: 'white' - }, - '& .MuiDataGrid-window': { - backgroundColor: 'rgba(0,0,0,0.54)' - } - } -}); - -export default TableStateProps; +import React, { useState, useEffect } from 'react'; +import { DataGrid, GridEditRowsModel } from '@mui/x-data-grid'; +import Button from '@mui/material/Button'; +import ClearIcon from '@mui/icons-material/Clear'; +import makeStyles from '@mui/styles/makeStyles'; +import { StatePropsPanelProps } from '../../../../interfaces/Interfaces'; +import { useDispatch, useSelector } from 'react-redux'; +import { deleteState } from '../../../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../../../redux/store'; +import { ColumnTab } from '../../../../interfaces/Interfaces'; +import { emitEvent } from '../../../../helperFunctions/socket'; + +// updates state mgmt boxes and data grid +const TableStateProps = (props) => { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + const classes = useStyles(); + const [editRowsModel] = useState({}); + const [gridColumns, setGridColumns] = useState([]); + const currentId = state.canvasFocus.componentId; + const currentComponent = state.components[currentId - 1]; + + // formatting for data grid columns + const columnTabs: ColumnTab[] = [ + { + field: 'id', + headerName: 'ID', + width: 30, + editable: false + }, + { + field: 'key', + headerName: 'Key', + width: 90, + editable: true + }, + { + field: 'value', + headerName: 'Initial Value', + width: 100, + editable: true, + valueGetter: (param) => { + //to display the actual object or array instead of [object Object], leave undefined if it is setter function + if (param.row.type === 'func') return; + return JSON.stringify(param.row.value); + } + }, + { + field: 'type', + headerName: 'Type', + width: 90, + editable: false + }, + { + field: 'delete', + headerName: 'X', + width: 30, + editable: false, + renderCell: function renderCell(params: any) { + return ( + + ); + } + } + ]; + + const handleDeleteState = (selectedId) => { + const currentId = state.canvasFocus.componentId; + const currentComponent = state.components[currentId - 1]; + const filtered = currentComponent.stateProps.filter( + (element) => element.id !== selectedId + ); + dispatch( + deleteState({ + stateProps: filtered, + rowId: selectedId, + contextParam: contextParam + }) + ); + + if (roomCode) { + emitEvent('deleteStateAction', roomCode, { + stateProps: filtered, + rowId: selectedId, + contextParam: contextParam + }); + } + }; + + useEffect(() => { + setGridColumns(columnTabs); + }, [props.isThemeLight]); + + const { selectHandler }: StatePropsPanelProps = props; + + useEffect(() => { + if (props.canDeleteState) { + setGridColumns(columnTabs); + } else { + setGridColumns(columnTabs.slice(0, gridColumns.length - 1)); + } + }, [state.canvasFocus.componentId]); + + // rows to show are either from current component or from a given provider + let rows = []; + currentComponent.stateProps?.forEach((prop) => { + rows.push(prop); + }); + + return ( +
    + +
    + ); +}; + +const useStyles = makeStyles({ + themeLight: { + color: 'white', + '& button:hover': { + backgroundColor: 'LightGray' + }, + '& button': { + color: 'white' + } + }, + themeDark: { + color: 'white', + '& .MuiTablePagination-root': { + color: 'white' + }, + '& .MuiIconButton-root': { + color: 'white' + }, + '& .MuiSvgIcon-root': { + color: 'white' + }, + '& .MuiDataGrid-window': { + backgroundColor: 'rgba(0,0,0,0.54)' + } + } +}); + +export default TableStateProps; diff --git a/app/src/components/StateManagement/DisplayTab/DataTable.tsx b/app/src/components/StateManagement/DisplayTab/DataTable.tsx index 6d066e906..41c18a9ce 100644 --- a/app/src/components/StateManagement/DisplayTab/DataTable.tsx +++ b/app/src/components/StateManagement/DisplayTab/DataTable.tsx @@ -1,111 +1,111 @@ -import React from 'react'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import { styled } from '@mui/material/styles'; -import TableCell, { tableCellClasses } from '@mui/material/TableCell'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../../redux/store' - -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: theme.palette.common.white, - }, - [`&.${tableCellClasses.body}`]: { - color: theme.palette.common.black, - fontSize: 14, - }, -})); - -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover - }, - // hide last border - '&:last-child td, &:last-child th': { - border: 0, - }, -})); - -export default function DataTable(props) { - const { - currComponentState, parentProps, clickedComp, data, - } = props; - const state = useSelector((store:RootState) => store.appState) - - // determine if the current component is a root component - let isRoot = false; - - for (let i = 0; i < data.length; i++) { - if (data[i].name === clickedComp) { - if (state.rootComponents.includes(data[i].id)) isRoot = true; - } - } - - return ( - - - - {/* we are checking if the clicked component is a root component-- if yes, it doesn't have any parents so don't need passed-in props table */} - {(!isRoot - && ( - <> - - - - Props Passed in from Parent: - - - - - - Key - Type - Initial Value - - {parentProps ? parentProps.map((data, index) => ( - - {data.key} - {data.type} - {data.value} - - )) : ''} - - - ) - )} - - {/* The below table will contain the state initialized within the clicked component */} - - - - State Initialized in Current Component: - - - - - - Key - Type - Initial Value - - {currComponentState ? currComponentState.map((data, index) => ( - - {data.key} - {data.type} - {data.value} - - )) : ''} - -
    -
    - ); +import React from 'react'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import { styled } from '@mui/material/styles'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + }, + [`&.${tableCellClasses.body}`]: { + color: theme.palette.common.black, + fontSize: 14, + }, +})); + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0, + }, +})); + +export default function DataTable(props) { + const { + currComponentState, parentProps, clickedComp, data, + } = props; + const state = useSelector((store:RootState) => store.appState) + + // determine if the current component is a root component + let isRoot = false; + + for (let i = 0; i < data.length; i++) { + if (data[i].name === clickedComp) { + if (state.rootComponents.includes(data[i].id)) isRoot = true; + } + } + + return ( + + + + {/* we are checking if the clicked component is a root component-- if yes, it doesn't have any parents so don't need passed-in props table */} + {(!isRoot + && ( + <> + + + + Props Passed in from Parent: + + + + + + Key + Type + Initial Value + + {parentProps ? parentProps.map((data, index) => ( + + {data.key} + {data.type} + {data.value} + + )) : ''} + + + ) + )} + + {/* The below table will contain the state initialized within the clicked component */} + + + + State Initialized in Current Component: + + + + + + Key + Type + Initial Value + + {currComponentState ? currComponentState.map((data, index) => ( + + {data.key} + {data.type} + {data.value} + + )) : ''} + +
    +
    + ); } \ No newline at end of file diff --git a/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx b/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx index 22a7d8ff1..fe9eb81f5 100644 --- a/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx +++ b/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx @@ -1,74 +1,74 @@ -import React, { useState } from 'react'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid'; -import { Typography } from '@mui/material'; -import DataTable from './DataTable'; -import Tree from './Tree'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../../redux/store'; - -function DisplayContainer({ data, props }) { - // "data" is referring to components from state - passed in from StateManagement - // grabbing intialized state from App using UseContext - const [currComponentState, setCurrComponentState] = useState([]); - const [parentProps, setParentProps] = useState([]); - const state = useSelector((store: RootState) => store.appState); - - let root = ''; - - // check the canvasFocus - // if canvasFocus is a root component, use that root component as "root" - if (state.rootComponents.includes(state.canvasFocus.componentId)) { - for (let i = 0; i < data.length; i++) { - if (data[i].id === state.canvasFocus.componentId) root = data[i].name; - } - } else if (state.projectType === 'Classic React') { - // else default to the main root component (aka app or index depending on react vs next/gatsby) - root = 'App'; - } else { - root = 'index'; - } - - // root becomes default value of clickedComp - const [clickedComp, setClickedComp] = useState(root); - - return ( -
    - - - - - Click on a component in the graph to see its state - - - Total State for {clickedComp} - - - -
    - ); -} -export default DisplayContainer; +import React, { useState } from 'react'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import { Typography } from '@mui/material'; +import DataTable from './DataTable'; +import Tree from './Tree'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store'; + +function DisplayContainer({ data, props }) { + // "data" is referring to components from state - passed in from StateManagement + // grabbing intialized state from App using UseContext + const [currComponentState, setCurrComponentState] = useState([]); + const [parentProps, setParentProps] = useState([]); + const state = useSelector((store: RootState) => store.appState); + + let root = ''; + + // check the canvasFocus + // if canvasFocus is a root component, use that root component as "root" + if (state.rootComponents.includes(state.canvasFocus.componentId)) { + for (let i = 0; i < data.length; i++) { + if (data[i].id === state.canvasFocus.componentId) root = data[i].name; + } + } else if (state.projectType === 'Classic React') { + // else default to the main root component (aka app or index depending on react vs next/gatsby) + root = 'App'; + } else { + root = 'index'; + } + + // root becomes default value of clickedComp + const [clickedComp, setClickedComp] = useState(root); + + return ( +
    + + + + + Click on a component in the graph to see its state + + + Total State for {clickedComp} + + + +
    + ); +} +export default DisplayContainer; diff --git a/app/src/components/StateManagement/DisplayTab/Tree.tsx b/app/src/components/StateManagement/DisplayTab/Tree.tsx index 415eacc1e..fa521151d 100644 --- a/app/src/components/StateManagement/DisplayTab/Tree.tsx +++ b/app/src/components/StateManagement/DisplayTab/Tree.tsx @@ -1,203 +1,203 @@ -import React, { useRef, useEffect} from 'react'; -import { - select, hierarchy, tree, linkHorizontal, -} from 'd3'; -import cloneDeep from 'lodash/cloneDeep'; -import useResizeObserver from './useResizeObserver'; -import { useSelector } from 'react-redux'; -import { ChildElement } from '../../../interfaces/Interfaces'; -import { RootState } from '../../../redux/store'; - -function usePrevious(value) { - const ref = useRef(); - useEffect(() => ref.current = value); - return ref.current; -} - -function Tree({ - data, setCurrComponentState, setParentProps, setClickedComp, -}) { - const state = useSelector((store:RootState) => store.appState) - // Provide types for the refs. - // In this case HTMLDivElement for the wrapperRef and SVGSVGElement for the svgRef. - // create mutable ref objects with initial values of null - const svgRef = useRef(null); - const wrapperRef = useRef(null); - const xPosition = 50; - const textAndBorderColor = '#bdbdbd'; - const dimensions = useResizeObserver(wrapperRef); - // we save data to see if it changed - const previouslyRenderedData = usePrevious(data); - // function to filter out separators to prevent render on tree chart - const removeHTMLElements = (arr: ChildElement[]) => { - for (let i = 0; i < arr.length; i++) { - if (arr[i] === undefined) continue; - // if element is separator, remove it - if (arr[i].type === 'HTML Element') { - arr.splice(i, 1); - i -= 1; - } - // if element has a children array and that array has length, recursive call - else if (arr[i].type === 'Component' && arr[i].children.length) { - // if element is a component, replace it with deep clone of latest version (to update with new HTML elements) - if (arr[i].type === 'Component') arr[i] = cloneDeep(data.find((component) => component.name === arr[i].name)); - removeHTMLElements(arr[i].children); - } - } - return arr; - }; - - // create a deep clone of data to avoid mutating the actual children array in removing separators - const dataDeepClone = cloneDeep(data); - - if (state.projectType === 'Next.js') { - dataDeepClone.forEach((element) => { - element.children = sanitize(element.children).filter((element) => !Array.isArray(element)); - }); - - function sanitize(children) { - return children.map((child) => { - if (child.name === 'Switch' || child.name === 'Route') return sanitize(child.children); - return child; - }); - } - } - - // remove separators and update components to current versions - dataDeepClone.forEach((component) => removeHTMLElements(component.children)); - - // will be called initially and on every data change - useEffect(() => { - const svg = select(svgRef.current); - // use dimensions from useResizeObserver, - // but use getBoundingClientRect on initial render - // (dimensions are null for the first render) - - const { width, height } = dimensions || wrapperRef.current.getBoundingClientRect(); - // transform hierarchical data - - let root; - let rootName; - - if (state.rootComponents.includes(state.canvasFocus.componentId)) { - // find out if canvasFocus is a root component - // if yes, set root of tree to be that canvasFocus component - // find that component inside dataDeepClone - for (let i = 0; i < dataDeepClone.length; i++) { - if (dataDeepClone[i].id === state.canvasFocus.componentId) { - root = hierarchy(dataDeepClone[i]); - rootName = dataDeepClone[i].name; - } - } - } else { - // if no, set root of tree to be app/index - root = hierarchy(dataDeepClone[0]); - rootName = dataDeepClone[0].name; - } - - setClickedComp(rootName); - - const treeLayout = tree().size([height, width - 125]); - // Returns a new link generator with horizontal display. - // To visualize links in a tree diagram rooted on the left edge of the display - const linkGenerator = linkHorizontal() - .x((link) => link.y) - .y((link) => link.x); - - // insert our data into the tree layout - treeLayout(root); - - svg - .selectAll('.node') - .data(root.descendants()) - .join((enter) => enter.append('circle').attr('opacity', 0)) - .attr('class', 'node') - /* - The cx, cy attributes are associated with the circle and ellipse elements and designate the centre of each shape. The coordinates are set from the top, left hand corner of the web page. - cx: The position of the centre of the element in the x axis measured from the left side of the screen. - cy: The position of the centre of the element in the y axis measured from the top of the screen. - */ - // translate (x, y) - .attr('cx', (node) => node.y) - .attr('cy', (node) => node.x) - .attr('r', 10) - .attr('opacity', 1) - .style('fill', 'white') - .attr('transform', `translate(${xPosition}, 0)`) - .on('click', (element) => { - const nameOfClicked = element.srcElement.__data__.data.name; - let passedInProps; - let componentStateProps; - - // iterate through data array to find stateProps and passedInProps - for (let i = 0; i < data.length; i++) { - if (data[i].name === nameOfClicked) { - componentStateProps = data[i].stateProps; - passedInProps = data[i].passedInProps; - } - } - setCurrComponentState(componentStateProps); - setParentProps(passedInProps); - setClickedComp(nameOfClicked); - }); - - // link - lines that connect the nodes - const enteringAndUpdatingLinks = svg - .selectAll('.link') - .data(root.links()) - .join('path') - .attr('class', 'link') - .attr('d', linkGenerator) - .attr('stroke', 'white') - .attr('fill', 'none') - .attr('opacity', 1) - .attr('transform', `translate(${xPosition}, 0)`); - if (data !== previouslyRenderedData) { - enteringAndUpdatingLinks - .attr('stroke-dashoffset', function () { - return this.length; - }) - .attr('stroke-dashoffset', 0); - } - - // label - the names of each html element (node) - svg - .selectAll('.label') - .data(root.descendants()) - .join((enter) => enter.append('text').attr('opacity', 0)) - .attr('class', 'label') - .attr('x', (node) => node.y) - .attr('y', (node) => node.x - 20) - .attr('text-anchor', 'middle') - .attr('font-size', 18) - .style('fill', 'white') - .text((node) => node.data.name) - .attr('opacity', 1) - .attr('transform', `translate(${xPosition}, 0)`); - }, [data, dimensions, previouslyRenderedData]); - - const treeStyles = { - height: '400px', - width: '100%', - margin: '10px 10px 10px 10px', - overflow: 'auto', - alignItems: 'center', - }; - - const wrapperStyles = { - borderRadius: '10px', - width: '100%', - height: '90%', - display: 'flex', - justifyContent: 'center', - backgroundColor: '#1E2024' - }; - - return ( -
    - -
    - ); -} - -export default Tree; +import React, { useRef, useEffect} from 'react'; +import { + select, hierarchy, tree, linkHorizontal, +} from 'd3'; +import cloneDeep from 'lodash/cloneDeep'; +import useResizeObserver from './useResizeObserver'; +import { useSelector } from 'react-redux'; +import { ChildElement } from '../../../interfaces/Interfaces'; +import { RootState } from '../../../redux/store'; + +function usePrevious(value) { + const ref = useRef(); + useEffect(() => ref.current = value); + return ref.current; +} + +function Tree({ + data, setCurrComponentState, setParentProps, setClickedComp, +}) { + const state = useSelector((store:RootState) => store.appState) + // Provide types for the refs. + // In this case HTMLDivElement for the wrapperRef and SVGSVGElement for the svgRef. + // create mutable ref objects with initial values of null + const svgRef = useRef(null); + const wrapperRef = useRef(null); + const xPosition = 50; + const textAndBorderColor = '#bdbdbd'; + const dimensions = useResizeObserver(wrapperRef); + // we save data to see if it changed + const previouslyRenderedData = usePrevious(data); + // function to filter out separators to prevent render on tree chart + const removeHTMLElements = (arr: ChildElement[]) => { + for (let i = 0; i < arr.length; i++) { + if (arr[i] === undefined) continue; + // if element is separator, remove it + if (arr[i].type === 'HTML Element') { + arr.splice(i, 1); + i -= 1; + } + // if element has a children array and that array has length, recursive call + else if (arr[i].type === 'Component' && arr[i].children.length) { + // if element is a component, replace it with deep clone of latest version (to update with new HTML elements) + if (arr[i].type === 'Component') arr[i] = cloneDeep(data.find((component) => component.name === arr[i].name)); + removeHTMLElements(arr[i].children); + } + } + return arr; + }; + + // create a deep clone of data to avoid mutating the actual children array in removing separators + const dataDeepClone = cloneDeep(data); + + if (state.projectType === 'Next.js') { + dataDeepClone.forEach((element) => { + element.children = sanitize(element.children).filter((element) => !Array.isArray(element)); + }); + + function sanitize(children) { + return children.map((child) => { + if (child.name === 'Switch' || child.name === 'Route') return sanitize(child.children); + return child; + }); + } + } + + // remove separators and update components to current versions + dataDeepClone.forEach((component) => removeHTMLElements(component.children)); + + // will be called initially and on every data change + useEffect(() => { + const svg = select(svgRef.current); + // use dimensions from useResizeObserver, + // but use getBoundingClientRect on initial render + // (dimensions are null for the first render) + + const { width, height } = dimensions || wrapperRef.current.getBoundingClientRect(); + // transform hierarchical data + + let root; + let rootName; + + if (state.rootComponents.includes(state.canvasFocus.componentId)) { + // find out if canvasFocus is a root component + // if yes, set root of tree to be that canvasFocus component + // find that component inside dataDeepClone + for (let i = 0; i < dataDeepClone.length; i++) { + if (dataDeepClone[i].id === state.canvasFocus.componentId) { + root = hierarchy(dataDeepClone[i]); + rootName = dataDeepClone[i].name; + } + } + } else { + // if no, set root of tree to be app/index + root = hierarchy(dataDeepClone[0]); + rootName = dataDeepClone[0].name; + } + + setClickedComp(rootName); + + const treeLayout = tree().size([height, width - 125]); + // Returns a new link generator with horizontal display. + // To visualize links in a tree diagram rooted on the left edge of the display + const linkGenerator = linkHorizontal() + .x((link) => link.y) + .y((link) => link.x); + + // insert our data into the tree layout + treeLayout(root); + + svg + .selectAll('.node') + .data(root.descendants()) + .join((enter) => enter.append('circle').attr('opacity', 0)) + .attr('class', 'node') + /* + The cx, cy attributes are associated with the circle and ellipse elements and designate the centre of each shape. The coordinates are set from the top, left hand corner of the web page. + cx: The position of the centre of the element in the x axis measured from the left side of the screen. + cy: The position of the centre of the element in the y axis measured from the top of the screen. + */ + // translate (x, y) + .attr('cx', (node) => node.y) + .attr('cy', (node) => node.x) + .attr('r', 10) + .attr('opacity', 1) + .style('fill', 'white') + .attr('transform', `translate(${xPosition}, 0)`) + .on('click', (element) => { + const nameOfClicked = element.srcElement.__data__.data.name; + let passedInProps; + let componentStateProps; + + // iterate through data array to find stateProps and passedInProps + for (let i = 0; i < data.length; i++) { + if (data[i].name === nameOfClicked) { + componentStateProps = data[i].stateProps; + passedInProps = data[i].passedInProps; + } + } + setCurrComponentState(componentStateProps); + setParentProps(passedInProps); + setClickedComp(nameOfClicked); + }); + + // link - lines that connect the nodes + const enteringAndUpdatingLinks = svg + .selectAll('.link') + .data(root.links()) + .join('path') + .attr('class', 'link') + .attr('d', linkGenerator) + .attr('stroke', 'white') + .attr('fill', 'none') + .attr('opacity', 1) + .attr('transform', `translate(${xPosition}, 0)`); + if (data !== previouslyRenderedData) { + enteringAndUpdatingLinks + .attr('stroke-dashoffset', function () { + return this.length; + }) + .attr('stroke-dashoffset', 0); + } + + // label - the names of each html element (node) + svg + .selectAll('.label') + .data(root.descendants()) + .join((enter) => enter.append('text').attr('opacity', 0)) + .attr('class', 'label') + .attr('x', (node) => node.y) + .attr('y', (node) => node.x - 20) + .attr('text-anchor', 'middle') + .attr('font-size', 18) + .style('fill', 'white') + .text((node) => node.data.name) + .attr('opacity', 1) + .attr('transform', `translate(${xPosition}, 0)`); + }, [data, dimensions, previouslyRenderedData]); + + const treeStyles = { + height: '400px', + width: '100%', + margin: '10px 10px 10px 10px', + overflow: 'auto', + alignItems: 'center', + }; + + const wrapperStyles = { + borderRadius: '10px', + width: '100%', + height: '90%', + display: 'flex', + justifyContent: 'center', + backgroundColor: '#1E2024' + }; + + return ( +
    + +
    + ); +} + +export default Tree; diff --git a/app/src/components/StateManagement/DisplayTab/useResizeObserver.ts b/app/src/components/StateManagement/DisplayTab/useResizeObserver.ts index d9adf16b1..74fb71fe0 100644 --- a/app/src/components/StateManagement/DisplayTab/useResizeObserver.ts +++ b/app/src/components/StateManagement/DisplayTab/useResizeObserver.ts @@ -1,23 +1,23 @@ -import { useEffect, useState } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; - -const useResizeObserver = (ref) => { - const [dimensions, setDimensions] = useState(null); - useEffect(() => { - // the element being observed (div with green border) - const observeTarget = ref.current; - const resizeObserver = new ResizeObserver((entries) => { - entries.forEach((entry) => { - // contentRect is an object containing the dimensions of the observed element - setDimensions(entry.contentRect); - }); - }); - resizeObserver.observe(observeTarget); - return () => { - resizeObserver.unobserve(observeTarget); - }; - }, [ref]); - return dimensions; -}; - -export default useResizeObserver; +import { useEffect, useState } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; + +const useResizeObserver = (ref) => { + const [dimensions, setDimensions] = useState(null); + useEffect(() => { + // the element being observed (div with green border) + const observeTarget = ref.current; + const resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + // contentRect is an object containing the dimensions of the observed element + setDimensions(entry.contentRect); + }); + }); + resizeObserver.observe(observeTarget); + return () => { + resizeObserver.unobserve(observeTarget); + }; + }, [ref]); + return dimensions; +}; + +export default useResizeObserver; diff --git a/app/src/components/StateManagement/StateManagement.tsx b/app/src/components/StateManagement/StateManagement.tsx index c86aaad2f..9fdde9e6e 100644 --- a/app/src/components/StateManagement/StateManagement.tsx +++ b/app/src/components/StateManagement/StateManagement.tsx @@ -1,66 +1,66 @@ -import React from 'react'; -import { makeStyles } from '@mui/styles'; -import Box from '@mui/material/Box'; -import Tab from '@mui/material/Tab'; -import TabContext from '@mui/lab/TabContext'; -import TabList from '@mui/lab/TabList'; -import TabPanel from '@mui/lab/TabPanel'; -import { useSelector } from 'react-redux'; -import CreateContainer from './CreateTab/CreateContainer'; -import DisplayContainer from './DisplayTab/DisplayContainer'; -import { RootState } from '../../redux/store'; - -const useStyles = makeStyles({ - contextContainer: { - backgroundColor: 'white', - height: 'fit-content', - width: 'fit-content', - minWidth: '100%' - } -}); - -const StateManager = (props): JSX.Element => { - const state = useSelector((store: RootState) => store.appState); - - const { components } = state; - const classes = useStyles(); - const [value, setValue] = React.useState('1'); - - const handleChange = (event: React.SyntheticEvent, newValue: string) => { - setValue(newValue); - }; - - // add hook here to access which component has been clicked - // then this will re-render the dataTable - - return ( - -
    - - - - - - - - - - - - - - - - -
    -
    - ); -}; - -export default StateManager; +import React from 'react'; +import { makeStyles } from '@mui/styles'; +import Box from '@mui/material/Box'; +import Tab from '@mui/material/Tab'; +import TabContext from '@mui/lab/TabContext'; +import TabList from '@mui/lab/TabList'; +import TabPanel from '@mui/lab/TabPanel'; +import { useSelector } from 'react-redux'; +import CreateContainer from './CreateTab/CreateContainer'; +import DisplayContainer from './DisplayTab/DisplayContainer'; +import { RootState } from '../../redux/store'; + +const useStyles = makeStyles({ + contextContainer: { + backgroundColor: 'white', + height: 'fit-content', + width: 'fit-content', + minWidth: '100%' + } +}); + +const StateManager = (props): JSX.Element => { + const state = useSelector((store: RootState) => store.appState); + + const { components } = state; + const classes = useStyles(); + const [value, setValue] = React.useState('1'); + + const handleChange = (event: React.SyntheticEvent, newValue: string) => { + setValue(newValue); + }; + + // add hook here to access which component has been clicked + // then this will re-render the dataTable + + return ( + +
    + + + + + + + + + + + + + + + + +
    +
    + ); +}; + +export default StateManager; diff --git a/app/src/components/bottom/BottomPanel.tsx b/app/src/components/bottom/BottomPanel.tsx index 753ff3a7f..a99971121 100644 --- a/app/src/components/bottom/BottomPanel.tsx +++ b/app/src/components/bottom/BottomPanel.tsx @@ -1,78 +1,78 @@ -import React, { useEffect, useRef, useState } from 'react'; -import BottomTabs from './BottomTabs'; -import { ExpandLess, ExpandMore } from '@mui/icons-material'; - -const BottomPanel = (props): JSX.Element => { - let y: number = 0; - let h: number = 0; - const node = useRef() as React.MutableRefObject; - - const [isDragging, setIsDragging] = useState(false); - - const mouseDownHandler = (e): void => { - y = e.clientY; - - const styles = window.getComputedStyle(node.current); - h = parseInt(styles.height, 10); - - document.addEventListener('mousemove', mouseMoveHandler); - document.addEventListener('mouseup', mouseUpHandler); - window.addEventListener('message', handleIframeMessage); //listens for messages from the iframe when the mouse is over it - }; - - //Interpret the messages from the iframe - const handleIframeMessage = (e) => { - if (e.data === 'iframeMouseUp') { - mouseUpHandler(); - } else if (e.data.type === 'iframeMouseMove') { - mouseMoveHandler(e.data); - } - }; - - const mouseMoveHandler = function (e: MouseEvent): void { - if (!props.bottomShow) return; // prevent drag calculation to occur when bottom menu is not showing - - const dy = y - e.clientY; - - const newVal = h + dy; - const styles = window.getComputedStyle(node.current); - const min = parseInt(styles.minHeight, 10); - node.current.style.height = newVal > min ? `${h + dy}px` : `${min}px`; - }; - - const mouseUpHandler = function () { - // puts false in callback queue after OnDragStart sets to true (b/c react 17 doesn't have onDragEnd) - setTimeout(() => setIsDragging(false), 0); - document.removeEventListener('mousemove', mouseMoveHandler); - document.removeEventListener('mouseup', mouseUpHandler); - window.removeEventListener('message', handleIframeMessage); - }; - - useEffect(() => { - node.current.style.height = '50vh'; - node.current.style.minHeight = '50vh'; - }, []); - - return ( - <> -
    -
    setIsDragging(true)} - onClick={() => !isDragging && props.setBottomShow(!props.bottomShow)} - tabIndex={0} - > - {props.bottomShow ? : } -
    - -
    - - ); -}; - -export default BottomPanel; +import React, { useEffect, useRef, useState } from 'react'; +import BottomTabs from './BottomTabs'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; + +const BottomPanel = (props): JSX.Element => { + let y: number = 0; + let h: number = 0; + const node = useRef() as React.MutableRefObject; + + const [isDragging, setIsDragging] = useState(false); + + const mouseDownHandler = (e): void => { + y = e.clientY; + + const styles = window.getComputedStyle(node.current); + h = parseInt(styles.height, 10); + + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); + window.addEventListener('message', handleIframeMessage); //listens for messages from the iframe when the mouse is over it + }; + + //Interpret the messages from the iframe + const handleIframeMessage = (e) => { + if (e.data === 'iframeMouseUp') { + mouseUpHandler(); + } else if (e.data.type === 'iframeMouseMove') { + mouseMoveHandler(e.data); + } + }; + + const mouseMoveHandler = function (e: MouseEvent): void { + if (!props.bottomShow) return; // prevent drag calculation to occur when bottom menu is not showing + + const dy = y - e.clientY; + + const newVal = h + dy; + const styles = window.getComputedStyle(node.current); + const min = parseInt(styles.minHeight, 10); + node.current.style.height = newVal > min ? `${h + dy}px` : `${min}px`; + }; + + const mouseUpHandler = function () { + // puts false in callback queue after OnDragStart sets to true (b/c react 17 doesn't have onDragEnd) + setTimeout(() => setIsDragging(false), 0); + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + window.removeEventListener('message', handleIframeMessage); + }; + + useEffect(() => { + node.current.style.height = '50vh'; + node.current.style.minHeight = '50vh'; + }, []); + + return ( + <> +
    +
    setIsDragging(true)} + onClick={() => !isDragging && props.setBottomShow(!props.bottomShow)} + tabIndex={0} + > + {props.bottomShow ? : } +
    + +
    + + ); +}; + +export default BottomPanel; diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index ecdb6d954..fa607dde4 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -1,250 +1,250 @@ -import React, { useState } from 'react'; -import makeStyles from '@mui/styles/makeStyles'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; -import StylesEditor from './StylesEditor'; -import CustomizationPanel from '../../containers/CustomizationPanel'; -import CreationPanel from './CreationPanel'; -import ContextManager from '../ContextAPIManager/ContextManager'; -import StateManager from '../StateManagement/StateManagement'; -import Chatroom from './ChatRoom'; -import Box from '@mui/material/Box'; -import Tree from '../../tree/TreeChart'; -import FormControl from '@mui/material/FormControl'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; -import arrow from '../main/Arrow'; -import { useDispatch, useSelector } from 'react-redux'; -import { changeProjectType } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; -import { MeetingProvider } from '@videosdk.live/react-sdk'; -const videoSDKToken = `${import.meta.env.VITE_VIDEOSDK_TOKEN}`; - -const BottomTabs = (props): JSX.Element => { - const { setBottomShow, isThemeLight } = props; - const dispatch = useDispatch(); - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const collaborationRoom = useSelector((store: RootState) => store.roomSlice); - - const [tab, setTab] = useState(0); - const classes = useStyles(); - const [theme, setTheme] = useState('solarized_light'); - - const handleChange = (event: React.ChangeEvent, value: number) => { - setTab(value); - }; - - const handleProjectChange = (event) => { - const projectType = event.target.value; - dispatch(changeProjectType({ projectType, contextParam })); - }; - const { components } = state; - - arrow.renderArrow(state.canvasFocus?.childId); - - const showBottomPanel = () => { - setBottomShow(true); - }; - - return ( - -
    - - - - - - - - - - - -
    - - - -
    -
    -
    - {tab === 0 && } - {tab === 1 && } - {tab === 2 && } - {tab === 3 && } - {tab === 4 && } - {tab === 5 && } - {tab === 6 && ( - - )} -
    -
    -
    - ); -}; - -const useStyles = makeStyles((theme) => ({ - root: { - flexGrow: 1, - height: '100%', - color: '#E8E8E8' - }, - rootLight: { - backgroundColor: '#0671e3' - }, - bottomHeader: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - Width: '200px' - }, - tabsRoot: { - minHeight: '50%' - }, - tabsIndicator: { - backgroundColor: '#0671E3' - }, - tabRoot: { - textTransform: 'initial', - minWidth: 170, - height: 60, - fontFamily: [ - '-apple-system', - 'BlinkMacSystemFont', - '"Segoe UI"', - 'Roboto', - '"Helvetica Neue"', - 'Arial', - 'sans-serif', - '"Apple Color Emoji"', - '"Segoe UI Emoji"', - '"Segoe UI Symbol"' - ].join(','), - '&:hover': { - color: 'white', - opacity: 1 - }, - fontWeight: 300, - '&$tabSelected': { - color: 'white', - backgroundColor: '#2D313A' - }, - '&:focus': { - color: 'white' - } - }, - tabSelected: {}, - typography: { - padding: '24px' - }, - padding: { - padding: `0 16px` - }, - switch: { - marginRight: '10px', - marginTop: '2px' - }, - projectTypeWrapper: { - marginTop: '10px', - marginBotton: '10px', - marginLeft: '10px' - }, - projectSelector: { - backgroundColor: '#131416', - color: 'white', - margin: '0 10px 10px 0' - } -})); - -export default BottomTabs; +import React, { useState } from 'react'; +import makeStyles from '@mui/styles/makeStyles'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import StylesEditor from './StylesEditor'; +import CustomizationPanel from '../../containers/CustomizationPanel'; +import CreationPanel from './CreationPanel'; +import ContextManager from '../ContextAPIManager/ContextManager'; +import StateManager from '../StateManagement/StateManagement'; +import Chatroom from './ChatRoom'; +import Box from '@mui/material/Box'; +import Tree from '../../tree/TreeChart'; +import FormControl from '@mui/material/FormControl'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import arrow from '../main/Arrow'; +import { useDispatch, useSelector } from 'react-redux'; +import { changeProjectType } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { MeetingProvider } from '@videosdk.live/react-sdk'; +const videoSDKToken = `${import.meta.env.VITE_VIDEOSDK_TOKEN}`; + +const BottomTabs = (props): JSX.Element => { + const { setBottomShow, isThemeLight } = props; + const dispatch = useDispatch(); + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const collaborationRoom = useSelector((store: RootState) => store.roomSlice); + + const [tab, setTab] = useState(0); + const classes = useStyles(); + const [theme, setTheme] = useState('solarized_light'); + + const handleChange = (event: React.ChangeEvent, value: number) => { + setTab(value); + }; + + const handleProjectChange = (event) => { + const projectType = event.target.value; + dispatch(changeProjectType({ projectType, contextParam })); + }; + const { components } = state; + + arrow.renderArrow(state.canvasFocus?.childId); + + const showBottomPanel = () => { + setBottomShow(true); + }; + + return ( + +
    + + + + + + + + + + + +
    + + + +
    +
    +
    + {tab === 0 && } + {tab === 1 && } + {tab === 2 && } + {tab === 3 && } + {tab === 4 && } + {tab === 5 && } + {tab === 6 && ( + + )} +
    +
    +
    + ); +}; + +const useStyles = makeStyles((theme) => ({ + root: { + flexGrow: 1, + height: '100%', + color: '#E8E8E8' + }, + rootLight: { + backgroundColor: '#0671e3' + }, + bottomHeader: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + Width: '200px' + }, + tabsRoot: { + minHeight: '50%' + }, + tabsIndicator: { + backgroundColor: '#0671E3' + }, + tabRoot: { + textTransform: 'initial', + minWidth: 170, + height: 60, + fontFamily: [ + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"' + ].join(','), + '&:hover': { + color: 'white', + opacity: 1 + }, + fontWeight: 300, + '&$tabSelected': { + color: 'white', + backgroundColor: '#2D313A' + }, + '&:focus': { + color: 'white' + } + }, + tabSelected: {}, + typography: { + padding: '24px' + }, + padding: { + padding: `0 16px` + }, + switch: { + marginRight: '10px', + marginTop: '2px' + }, + projectTypeWrapper: { + marginTop: '10px', + marginBotton: '10px', + marginLeft: '10px' + }, + projectSelector: { + backgroundColor: '#131416', + color: 'white', + margin: '0 10px 10px 0' + } +})); + +export default BottomTabs; diff --git a/app/src/components/bottom/ChatRoom.tsx b/app/src/components/bottom/ChatRoom.tsx index 128b2845b..bfac4968d 100644 --- a/app/src/components/bottom/ChatRoom.tsx +++ b/app/src/components/bottom/ChatRoom.tsx @@ -1,237 +1,237 @@ -import { useState, useRef, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../redux/store'; -import { emitEvent } from '../../helperFunctions/socket'; -import Videomeeting from './VideoMeeting'; -import { Send } from '@mui/icons-material'; - -const Chatroom = (props): JSX.Element => { - const { userName, roomCode, messages, userJoinCollabRoom } = useSelector( - (store: RootState) => store.roomSlice - ); - - const [inputContent, setInputContent] = useState(''); - - const wrapperStyles = { - border: '1px solid #31343A', - borderRadius: '15px', - width: '70%', - height: '100%', - display: 'flex', - flexDirection: 'column', - alignSelf: 'center', - padding: '12px 20px', - backgroundColor: '#1B1B1B', - overflow: 'auto' - }; - - const inputContainerStyles = { - width: '100%', - paddingLeft: '20px', - paddingTop: '10px', - display: 'flex', - justifyContent: 'center' - }; - - const inputStyles = { - width: '72%', - padding: '10px 12px', - borderRadius: '50px', - backgroundColor: '#1B1B1B', - color: 'white', - border: '1px solid #31343A', - marginLeft: '28px' - }; - - const buttonStyles = { - padding: '5px 7px', - marginLeft: '10px', - backgroundColor: '#0671E3', - color: 'white', - border: 'none', - borderRadius: '50%', - cursor: 'pointer' - }; - - const handleSubmit = (e) => { - e.preventDefault(); - if (inputContent !== '') { - emitEvent('send-chat-message', roomCode, { - userName, - message: inputContent - }); - setInputContent(''); - } - }; - - const handleMessageContainerStyle = (message: object) => { - if (message['type'] === 'activity') { - return { - color: '#E8E9EB', - fontSize: '12px', - alignSelf: 'center', - margin: '3px 0' - }; - } else { - if (message['userName'] === userName) - return { - alignSelf: 'end', - padding: '8px 15px', - backgroundColor: '#0671E3', - borderRadius: '15.5px', - margin: '3px 0', - maxWidth: '300px' - }; - return { - color: 'white', - padding: '8px 15px', - backgroundColor: '#333333', - borderRadius: '15.5px', - margin: '3px 0', - maxWidth: '300px' - }; - } - }; - - const renderMessages = () => { - return messages.map((message, index) => { - if (message.type === 'activity') { - return ( -
    - {message.message} -
    - ); - } else if (message.type === 'chat') { - if (message.userName === userName) { - return ( -
    - {message.message} -
    - ); - } else - return ( -
    -
    - {message.userName} -
    -
    - {message.message} -
    -
    - ); - } - return null; - }); - }; - - const containerRef = useRef(null); - - // Scroll to the bottom of the container whenever new messages are added - useEffect(() => { - if (containerRef.current) { - containerRef.current.scrollTop = containerRef.current.scrollHeight; - } - }, [messages]); - - return ( -
    -
    - - {userJoinCollabRoom && ( -
    -
    -
    - {renderMessages()} -
    -
    -
    - setInputContent(e.target.value)} - value={inputContent} - style={inputStyles} - /> - -
    -
    -
    -
    - )} -
    - {!userJoinCollabRoom && ( -
    -

    - Please join a collaboration room to enable this function -

    -
    - )} -
    - ); -}; -export default Chatroom; +import { useState, useRef, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; +import Videomeeting from './VideoMeeting'; +import { Send } from '@mui/icons-material'; + +const Chatroom = (props): JSX.Element => { + const { userName, roomCode, messages, userJoinCollabRoom } = useSelector( + (store: RootState) => store.roomSlice + ); + + const [inputContent, setInputContent] = useState(''); + + const wrapperStyles = { + border: '1px solid #31343A', + borderRadius: '15px', + width: '70%', + height: '100%', + display: 'flex', + flexDirection: 'column', + alignSelf: 'center', + padding: '12px 20px', + backgroundColor: '#1B1B1B', + overflow: 'auto' + }; + + const inputContainerStyles = { + width: '100%', + paddingLeft: '20px', + paddingTop: '10px', + display: 'flex', + justifyContent: 'center' + }; + + const inputStyles = { + width: '72%', + padding: '10px 12px', + borderRadius: '50px', + backgroundColor: '#1B1B1B', + color: 'white', + border: '1px solid #31343A', + marginLeft: '28px' + }; + + const buttonStyles = { + padding: '5px 7px', + marginLeft: '10px', + backgroundColor: '#0671E3', + color: 'white', + border: 'none', + borderRadius: '50%', + cursor: 'pointer' + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (inputContent !== '') { + emitEvent('send-chat-message', roomCode, { + userName, + message: inputContent + }); + setInputContent(''); + } + }; + + const handleMessageContainerStyle = (message: object) => { + if (message['type'] === 'activity') { + return { + color: '#E8E9EB', + fontSize: '12px', + alignSelf: 'center', + margin: '3px 0' + }; + } else { + if (message['userName'] === userName) + return { + alignSelf: 'end', + padding: '8px 15px', + backgroundColor: '#0671E3', + borderRadius: '15.5px', + margin: '3px 0', + maxWidth: '300px' + }; + return { + color: 'white', + padding: '8px 15px', + backgroundColor: '#333333', + borderRadius: '15.5px', + margin: '3px 0', + maxWidth: '300px' + }; + } + }; + + const renderMessages = () => { + return messages.map((message, index) => { + if (message.type === 'activity') { + return ( +
    + {message.message} +
    + ); + } else if (message.type === 'chat') { + if (message.userName === userName) { + return ( +
    + {message.message} +
    + ); + } else + return ( +
    +
    + {message.userName} +
    +
    + {message.message} +
    +
    + ); + } + return null; + }); + }; + + const containerRef = useRef(null); + + // Scroll to the bottom of the container whenever new messages are added + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [messages]); + + return ( +
    +
    + + {userJoinCollabRoom && ( +
    +
    +
    + {renderMessages()} +
    +
    +
    + setInputContent(e.target.value)} + value={inputContent} + style={inputStyles} + /> + +
    +
    +
    +
    + )} +
    + {!userJoinCollabRoom && ( +
    +

    + Please join a collaboration room to enable this function +

    +
    + )} +
    + ); +}; +export default Chatroom; diff --git a/app/src/components/bottom/CodePreview.tsx b/app/src/components/bottom/CodePreview.tsx index 74edf7200..07c223262 100644 --- a/app/src/components/bottom/CodePreview.tsx +++ b/app/src/components/bottom/CodePreview.tsx @@ -1,114 +1,114 @@ -import 'ace-builds/src-noconflict/ace'; -import 'ace-builds/src-min-noconflict/ext-searchbox'; -import 'ace-builds/src-noconflict/mode-javascript'; -import 'ace-builds/src-noconflict/theme-dracula'; -import 'ace-builds/src-noconflict/theme-clouds_midnight'; - -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { - codePreviewInput, - codePreviewSave -} from '../../redux/reducers/slice/codePreviewSlice'; -import { useDispatch, useSelector } from 'react-redux'; - -import AceEditor from 'react-ace'; -import { Component } from '../../interfaces/Interfaces'; -import { RootState } from '../../redux/store'; -import { fetchPlugin } from '../../plugins/fetch-plugin'; -import { unpkgPathPlugin } from '../../plugins/unpkg-path-plugin'; -import useResizeObserver from '../../tree/useResizeObserver'; -import { initializeEsbuild } from '../../helperFunctions/esbuildService'; - -const CodePreview: React.FC<{ - theme: string | null; - setTheme: any | null; - // zoom: number; // This is added if you want the Code Editor to zoom in/out - containerRef: any; -}> = ({ theme, setTheme, zoom, containerRef }) => { - const ref = useRef(); - - const dispatch = useDispatch(); - - const wrapper = useRef(); - const dimensions = useResizeObserver(wrapper); - const { height } = dimensions || 0; - const state = useSelector((store: RootState) => store.appState); - const [, setDivHeight] = useState(0); - let currentComponent = state.components.find( - (elem: Component) => elem.id === state.canvasFocus.componentId - ); - - const [input, setInput] = useState(''); - - useEffect(() => { - //Starts the Web Assembly service - initializeEsbuild(); - }, []); - - useEffect(() => { - setDivHeight(height); - }, [height]); - - useEffect(() => { - setInput(currentComponent.code); - dispatch(codePreviewInput(currentComponent.code)); - }, [currentComponent, state.components]); - - /** - * Handler thats listens to changes in code editor - * @param {string} data - Code entered by the user - */ - const handleChange = async (data) => { - setInput(data); - dispatch(codePreviewInput(data)); - if (!ref.current) { - return; - } - let result = await ref.current.build({ - entryPoints: ['index.js'], - bundle: true, - write: false, - incremental: true, - minify: true, - plugins: [unpkgPathPlugin(), fetchPlugin(data)], - define: { - 'import.meta.env.NODE_ENV': '"production"', - global: 'window' - } - }); - dispatch(codePreviewSave(result.outputFiles[0].text)); - }; - - return ( -
    -
    - -
    - ); -}; - -export default CodePreview; +import 'ace-builds/src-noconflict/ace'; +import 'ace-builds/src-min-noconflict/ext-searchbox'; +import 'ace-builds/src-noconflict/mode-javascript'; +import 'ace-builds/src-noconflict/theme-dracula'; +import 'ace-builds/src-noconflict/theme-clouds_midnight'; + +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { + codePreviewInput, + codePreviewSave +} from '../../redux/reducers/slice/codePreviewSlice'; +import { useDispatch, useSelector } from 'react-redux'; + +import AceEditor from 'react-ace'; +import { Component } from '../../interfaces/Interfaces'; +import { RootState } from '../../redux/store'; +import { fetchPlugin } from '../../plugins/fetch-plugin'; +import { unpkgPathPlugin } from '../../plugins/unpkg-path-plugin'; +import useResizeObserver from '../../tree/useResizeObserver'; +import { initializeEsbuild } from '../../helperFunctions/esbuildService'; + +const CodePreview: React.FC<{ + theme: string | null; + setTheme: any | null; + // zoom: number; // This is added if you want the Code Editor to zoom in/out + containerRef: any; +}> = ({ theme, setTheme, zoom, containerRef }) => { + const ref = useRef(); + + const dispatch = useDispatch(); + + const wrapper = useRef(); + const dimensions = useResizeObserver(wrapper); + const { height } = dimensions || 0; + const state = useSelector((store: RootState) => store.appState); + const [, setDivHeight] = useState(0); + let currentComponent = state.components.find( + (elem: Component) => elem.id === state.canvasFocus.componentId + ); + + const [input, setInput] = useState(''); + + useEffect(() => { + //Starts the Web Assembly service + initializeEsbuild(); + }, []); + + useEffect(() => { + setDivHeight(height); + }, [height]); + + useEffect(() => { + setInput(currentComponent.code); + dispatch(codePreviewInput(currentComponent.code)); + }, [currentComponent, state.components]); + + /** + * Handler thats listens to changes in code editor + * @param {string} data - Code entered by the user + */ + const handleChange = async (data) => { + setInput(data); + dispatch(codePreviewInput(data)); + if (!ref.current) { + return; + } + let result = await ref.current.build({ + entryPoints: ['index.js'], + bundle: true, + write: false, + incremental: true, + minify: true, + plugins: [unpkgPathPlugin(), fetchPlugin(data)], + define: { + 'import.meta.env.NODE_ENV': '"production"', + global: 'window' + } + }); + dispatch(codePreviewSave(result.outputFiles[0].text)); + }; + + return ( +
    +
    + +
    + ); +}; + +export default CodePreview; diff --git a/app/src/components/bottom/CreationPanel.tsx b/app/src/components/bottom/CreationPanel.tsx index d00616ada..65e8472bd 100644 --- a/app/src/components/bottom/CreationPanel.tsx +++ b/app/src/components/bottom/CreationPanel.tsx @@ -1,19 +1,19 @@ -import React from 'react'; -import ComponentPanel from '../right/ComponentPanel'; -import HTMLPanel from '../left/HTMLPanel'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../redux/store'; - -// Creation panel holds all of the creation functionality of the application. ComponentPanel, HTMLPanel, and StatePropsPanel are all hanged here. -// This allows users to create all aspects of this application in one place. -const CreationPanel = (props): JSX.Element => { - const style = useSelector((store: RootState) => store.styleSlice); - return ( -
    - - -
    - ); -}; - -export default CreationPanel; +import React from 'react'; +import ComponentPanel from '../right/ComponentPanel'; +import HTMLPanel from '../left/HTMLPanel'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; + +// Creation panel holds all of the creation functionality of the application. ComponentPanel, HTMLPanel, and StatePropsPanel are all hanged here. +// This allows users to create all aspects of this application in one place. +const CreationPanel = (props): JSX.Element => { + const style = useSelector((store: RootState) => store.styleSlice); + return ( +
    + + +
    + ); +}; + +export default CreationPanel; diff --git a/app/src/components/bottom/StylesEditor.tsx b/app/src/components/bottom/StylesEditor.tsx index 2de9d1a11..e46d062ed 100644 --- a/app/src/components/bottom/StylesEditor.tsx +++ b/app/src/components/bottom/StylesEditor.tsx @@ -1,84 +1,84 @@ -import 'ace-builds/src-noconflict/ace'; -import 'ace-builds/src-noconflict/mode-css'; -import 'ace-builds/src-noconflict/theme-dracula'; -import 'ace-builds/src-min-noconflict/ext-searchbox'; -import 'ace-builds/src-noconflict/ext-language_tools'; - -import React, { useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import AceEditor from 'react-ace'; -import Fab from '@mui/material/Fab'; -import { RootState } from '../../redux/store'; -import SaveIcon from '@mui/icons-material/Save'; -import { updateStylesheet } from '../../redux/reducers/slice/appStateSlice'; -import { emitEvent } from '../../helperFunctions/socket'; - -const StylesEditor: React.FC<{ - theme: string | null; - setTheme: any | null; -}> = ({ theme, setTheme }) => { - const wrapper = useRef(); - const stylesheet = useSelector( - (state: RootState) => state.appState.stylesheet - ); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - //sets state for what text is currently in the csseditor - const [css, setCss] = useState(stylesheet); - - const dispatch = useDispatch(); - - //on save, updates the state based on above hook and rerenders the demo - const saveCss = (e) => { - e.preventDefault(); - dispatch(updateStylesheet(css)); - if (roomCode) { - emitEvent('updateCSSAction', roomCode, css); - } - }; - - //handles changes in the ace editor - const handleChange = (text) => { - setCss(text); - }; - - return ( -
    - - - - -
    - ); -}; - -export default StylesEditor; +import 'ace-builds/src-noconflict/ace'; +import 'ace-builds/src-noconflict/mode-css'; +import 'ace-builds/src-noconflict/theme-dracula'; +import 'ace-builds/src-min-noconflict/ext-searchbox'; +import 'ace-builds/src-noconflict/ext-language_tools'; + +import React, { useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import AceEditor from 'react-ace'; +import Fab from '@mui/material/Fab'; +import { RootState } from '../../redux/store'; +import SaveIcon from '@mui/icons-material/Save'; +import { updateStylesheet } from '../../redux/reducers/slice/appStateSlice'; +import { emitEvent } from '../../helperFunctions/socket'; + +const StylesEditor: React.FC<{ + theme: string | null; + setTheme: any | null; +}> = ({ theme, setTheme }) => { + const wrapper = useRef(); + const stylesheet = useSelector( + (state: RootState) => state.appState.stylesheet + ); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + //sets state for what text is currently in the csseditor + const [css, setCss] = useState(stylesheet); + + const dispatch = useDispatch(); + + //on save, updates the state based on above hook and rerenders the demo + const saveCss = (e) => { + e.preventDefault(); + dispatch(updateStylesheet(css)); + if (roomCode) { + emitEvent('updateCSSAction', roomCode, css); + } + }; + + //handles changes in the ace editor + const handleChange = (text) => { + setCss(text); + }; + + return ( +
    + + + + +
    + ); +}; + +export default StylesEditor; diff --git a/app/src/components/bottom/UseStateModal.tsx b/app/src/components/bottom/UseStateModal.tsx index adc7774d2..0a55704b1 100644 --- a/app/src/components/bottom/UseStateModal.tsx +++ b/app/src/components/bottom/UseStateModal.tsx @@ -1,63 +1,63 @@ -import React, { useState, useRef } from 'react'; -import Modal from '@mui/material/Modal'; -import TableStateProps from '../StateManagement/CreateTab/components/TableStateProps'; - -function UseStateModal({ updateAttributeWithState, attributeToChange }) { - const [open, setOpen] = useState(false); - const [stateKey, setStateKey] = useState(''); - const [statePropsId, setStatePropsId] = useState(-1); - const [componentProviderId, setComponentProviderId] = useState(1); - const container = useRef(null); - // table to choose state from - const body = ( -
    -
    - Choose State - -
    -
    -
    - { - updateAttributeWithState( - attributeToChange, - componentProviderId, - statePropsId > 0 ? statePropsId : table.row.id, - table.row, - stateKey + table.row.key - ); - setStateKey(''); - setStatePropsId(-1); - setOpen(false); - }} - isThemeLight={true} - /> -
    -
    -
    - ); - - return ( -
    - - - {body} - -
    - ); -} - -export default UseStateModal; +import React, { useState, useRef } from 'react'; +import Modal from '@mui/material/Modal'; +import TableStateProps from '../StateManagement/CreateTab/components/TableStateProps'; + +function UseStateModal({ updateAttributeWithState, attributeToChange }) { + const [open, setOpen] = useState(false); + const [stateKey, setStateKey] = useState(''); + const [statePropsId, setStatePropsId] = useState(-1); + const [componentProviderId, setComponentProviderId] = useState(1); + const container = useRef(null); + // table to choose state from + const body = ( +
    +
    + Choose State + +
    +
    +
    + { + updateAttributeWithState( + attributeToChange, + componentProviderId, + statePropsId > 0 ? statePropsId : table.row.id, + table.row, + stateKey + table.row.key + ); + setStateKey(''); + setStatePropsId(-1); + setOpen(false); + }} + isThemeLight={true} + /> +
    +
    +
    + ); + + return ( +
    + + + {body} + +
    + ); +} + +export default UseStateModal; diff --git a/app/src/components/bottom/VideoMeeting.tsx b/app/src/components/bottom/VideoMeeting.tsx index 097941f93..de61a861e 100644 --- a/app/src/components/bottom/VideoMeeting.tsx +++ b/app/src/components/bottom/VideoMeeting.tsx @@ -1,257 +1,257 @@ -import { useState, useRef, useEffect, useMemo } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState } from '../../redux/store'; -import { - setUserJoinMeetingStatus, - setMeetingParticipants, - setUseMic, - setUseWebcam -} from '../../redux/reducers/slice/roomSlice'; -import { - MeetingConsumer, - useMeeting, - useParticipant -} from '@videosdk.live/react-sdk'; -import ReactPlayer from 'react-player'; -import Button from '@mui/material/Button'; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; -import MicOffIcon from '@mui/icons-material/MicOff'; -import MicIcon from '@mui/icons-material/Mic'; -import VideocamIcon from '@mui/icons-material/Videocam'; -import VideocamOffIcon from '@mui/icons-material/VideocamOff'; -import VideoMeetingControl from './VideoMeetingControl'; - -const Videomeeting = (props): JSX.Element => { - const dispatch = useDispatch(); - const { - meetingId, - userJoinCollabRoom, - userJoinMeetingStatus, - meetingParticipants, - useMic, - useWebcam - } = useSelector((store: RootState) => store.roomSlice); - - const micRef = useRef(null); - - const TurnOffCameraDisplay = () => { - return ( -
    - -
    - ); - }; - - const onMeetingLeave = () => { - dispatch(setUserJoinMeetingStatus(null)); - dispatch(setUseWebcam(false)); - dispatch(setUseMic(false)); - }; - - const handleUserInfoStyle = (isLocalParticipant: boolean) => { - if (isLocalParticipant) return { color: '#0671E3', alignItems: 'center' }; - else return { color: 'white', alignItems: 'center' }; - }; - - const ParticipantView = ({ participantId, isLocalParticipant }) => { - const { webcamStream, micStream, webcamOn, micOn, isLocal, displayName } = - useParticipant(participantId); - - const videoStream = useMemo(() => { - if (webcamOn && webcamStream) { - const mediaStream = new MediaStream(); - mediaStream.addTrack(webcamStream.track); - return mediaStream; - } - }, [webcamStream, webcamOn]); - - useEffect(() => { - if (micRef.current) { - if (micOn && micStream) { - const mediaStream = new MediaStream(); - mediaStream.addTrack(micStream.track); - - micRef.current.srcObject = mediaStream; - - micRef.current - .play() - .catch((error) => - console.error('videoElem.current.play() failed', error) - ); - } else { - micRef.current.srcObject = null; - } - } - }, [micStream, micOn]); - - return ( - <> - {userJoinMeetingStatus === 'JOINED' && ( - <> -
    -
    - - )} - - ); - }; - - const MeetingView = ({ onMeetingLeave }) => { - const { join, localParticipant, leave } = useMeeting(); - - const { participants } = useMeeting({ - onMeetingJoined: () => { - dispatch(setUserJoinMeetingStatus('JOINED')); - }, - onMeetingLeft: () => { - onMeetingLeave(); - } - }); - - const meetingParticipantsId = [...participants.keys()]; - - if ( - JSON.stringify(meetingParticipantsId) !== - JSON.stringify(meetingParticipants) && - meetingParticipantsId.length > 0 - ) { - dispatch(setMeetingParticipants(meetingParticipantsId)); - } - - const joinMeeting = () => { - dispatch(setUserJoinMeetingStatus('JOINING')); - join(); - }; - - if (!userJoinCollabRoom && userJoinMeetingStatus !== null) { - leave(); - onMeetingLeave(); - dispatch(setUserJoinMeetingStatus(null)); - } - - return ( -
    -
    - -
    - {[...meetingParticipantsId].map((participantId, idx) => ( - - ))} -
    -
    - {userJoinMeetingStatus === 'JOINING' &&

    Joining the meeting...

    } - {userJoinCollabRoom && userJoinMeetingStatus === null && ( - - )} -
    - ); - }; - - return ( - meetingId && ( - - {() => } - - ) - ); -}; - -export default Videomeeting; +import { useState, useRef, useEffect, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../redux/store'; +import { + setUserJoinMeetingStatus, + setMeetingParticipants, + setUseMic, + setUseWebcam +} from '../../redux/reducers/slice/roomSlice'; +import { + MeetingConsumer, + useMeeting, + useParticipant +} from '@videosdk.live/react-sdk'; +import ReactPlayer from 'react-player'; +import Button from '@mui/material/Button'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import MicOffIcon from '@mui/icons-material/MicOff'; +import MicIcon from '@mui/icons-material/Mic'; +import VideocamIcon from '@mui/icons-material/Videocam'; +import VideocamOffIcon from '@mui/icons-material/VideocamOff'; +import VideoMeetingControl from './VideoMeetingControl'; + +const Videomeeting = (props): JSX.Element => { + const dispatch = useDispatch(); + const { + meetingId, + userJoinCollabRoom, + userJoinMeetingStatus, + meetingParticipants, + useMic, + useWebcam + } = useSelector((store: RootState) => store.roomSlice); + + const micRef = useRef(null); + + const TurnOffCameraDisplay = () => { + return ( +
    + +
    + ); + }; + + const onMeetingLeave = () => { + dispatch(setUserJoinMeetingStatus(null)); + dispatch(setUseWebcam(false)); + dispatch(setUseMic(false)); + }; + + const handleUserInfoStyle = (isLocalParticipant: boolean) => { + if (isLocalParticipant) return { color: '#0671E3', alignItems: 'center' }; + else return { color: 'white', alignItems: 'center' }; + }; + + const ParticipantView = ({ participantId, isLocalParticipant }) => { + const { webcamStream, micStream, webcamOn, micOn, isLocal, displayName } = + useParticipant(participantId); + + const videoStream = useMemo(() => { + if (webcamOn && webcamStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(webcamStream.track); + return mediaStream; + } + }, [webcamStream, webcamOn]); + + useEffect(() => { + if (micRef.current) { + if (micOn && micStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(micStream.track); + + micRef.current.srcObject = mediaStream; + + micRef.current + .play() + .catch((error) => + console.error('videoElem.current.play() failed', error) + ); + } else { + micRef.current.srcObject = null; + } + } + }, [micStream, micOn]); + + return ( + <> + {userJoinMeetingStatus === 'JOINED' && ( + <> +
    +
    + + )} + + ); + }; + + const MeetingView = ({ onMeetingLeave }) => { + const { join, localParticipant, leave } = useMeeting(); + + const { participants } = useMeeting({ + onMeetingJoined: () => { + dispatch(setUserJoinMeetingStatus('JOINED')); + }, + onMeetingLeft: () => { + onMeetingLeave(); + } + }); + + const meetingParticipantsId = [...participants.keys()]; + + if ( + JSON.stringify(meetingParticipantsId) !== + JSON.stringify(meetingParticipants) && + meetingParticipantsId.length > 0 + ) { + dispatch(setMeetingParticipants(meetingParticipantsId)); + } + + const joinMeeting = () => { + dispatch(setUserJoinMeetingStatus('JOINING')); + join(); + }; + + if (!userJoinCollabRoom && userJoinMeetingStatus !== null) { + leave(); + onMeetingLeave(); + dispatch(setUserJoinMeetingStatus(null)); + } + + return ( +
    +
    + +
    + {[...meetingParticipantsId].map((participantId, idx) => ( + + ))} +
    +
    + {userJoinMeetingStatus === 'JOINING' &&

    Joining the meeting...

    } + {userJoinCollabRoom && userJoinMeetingStatus === null && ( + + )} +
    + ); + }; + + return ( + meetingId && ( + + {() => } + + ) + ); +}; + +export default Videomeeting; diff --git a/app/src/components/bottom/VideoMeetingControl.tsx b/app/src/components/bottom/VideoMeetingControl.tsx index b8de16245..24f046eeb 100644 --- a/app/src/components/bottom/VideoMeetingControl.tsx +++ b/app/src/components/bottom/VideoMeetingControl.tsx @@ -1,176 +1,176 @@ -import React, { useState, useCallback } from 'react'; -import { useMeeting } from '@videosdk.live/react-sdk'; -import { useSelector, useDispatch } from 'react-redux'; -import CallEndIcon from '@mui/icons-material/CallEnd'; -import MicOffIcon from '@mui/icons-material/MicOff'; -import MicIcon from '@mui/icons-material/Mic'; -import VideocamIcon from '@mui/icons-material/Videocam'; -import VideocamOffIcon from '@mui/icons-material/VideocamOff'; - -import { setUseMic, setUseWebcam } from '../../redux/reducers/slice/roomSlice'; -import { RootState } from '../../redux/store'; - -interface VideoMeetingControlProps { - userJoinMeetingStatus: string; - useWebcam: boolean; - useMic: boolean; -} - -enum ButtonType { - CALL_END = 'Call End', - MIC = 'Mic', - WEBCAM = 'Webcam' -} - -const VideoMeetingControl: React.FC = () => { - const { leave, toggleMic, toggleWebcam } = useMeeting(); - - const [callEndHovered, setCallEndHovered] = useState(false); - const [micHovered, setMicHovered] = useState(false); - const [webcamHovered, setWebcamHovered] = useState(false); - - const dispatch = useDispatch(); - const { userJoinMeetingStatus, useMic, useWebcam } = useSelector( - (store: RootState) => store.roomSlice - ); - - const handleButtonHover = useCallback((button: string, hovered: boolean) => { - switch (button) { - case ButtonType.CALL_END: - setCallEndHovered(hovered); - break; - case ButtonType.MIC: - setMicHovered(hovered); - break; - default: - setWebcamHovered(hovered); - } - }, []); - - return ( - userJoinMeetingStatus === 'JOINED' && ( -
    - {/* Mic Button */} -
    handleButtonHover(ButtonType.MIC, true)} - onMouseLeave={() => handleButtonHover(ButtonType.MIC, false)} - > - -
    - {/* Webcam Button */} -
    handleButtonHover(ButtonType.WEBCAM, true)} - onMouseLeave={() => handleButtonHover(ButtonType.WEBCAM, false)} - > - -
    - {/* Call End Button */} -
    handleButtonHover(ButtonType.CALL_END, true)} - onMouseLeave={() => handleButtonHover(ButtonType.CALL_END, false)} - > - -
    -
    - ) - ); -}; - -export default VideoMeetingControl; +import React, { useState, useCallback } from 'react'; +import { useMeeting } from '@videosdk.live/react-sdk'; +import { useSelector, useDispatch } from 'react-redux'; +import CallEndIcon from '@mui/icons-material/CallEnd'; +import MicOffIcon from '@mui/icons-material/MicOff'; +import MicIcon from '@mui/icons-material/Mic'; +import VideocamIcon from '@mui/icons-material/Videocam'; +import VideocamOffIcon from '@mui/icons-material/VideocamOff'; + +import { setUseMic, setUseWebcam } from '../../redux/reducers/slice/roomSlice'; +import { RootState } from '../../redux/store'; + +interface VideoMeetingControlProps { + userJoinMeetingStatus: string; + useWebcam: boolean; + useMic: boolean; +} + +enum ButtonType { + CALL_END = 'Call End', + MIC = 'Mic', + WEBCAM = 'Webcam' +} + +const VideoMeetingControl: React.FC = () => { + const { leave, toggleMic, toggleWebcam } = useMeeting(); + + const [callEndHovered, setCallEndHovered] = useState(false); + const [micHovered, setMicHovered] = useState(false); + const [webcamHovered, setWebcamHovered] = useState(false); + + const dispatch = useDispatch(); + const { userJoinMeetingStatus, useMic, useWebcam } = useSelector( + (store: RootState) => store.roomSlice + ); + + const handleButtonHover = useCallback((button: string, hovered: boolean) => { + switch (button) { + case ButtonType.CALL_END: + setCallEndHovered(hovered); + break; + case ButtonType.MIC: + setMicHovered(hovered); + break; + default: + setWebcamHovered(hovered); + } + }, []); + + return ( + userJoinMeetingStatus === 'JOINED' && ( +
    + {/* Mic Button */} +
    handleButtonHover(ButtonType.MIC, true)} + onMouseLeave={() => handleButtonHover(ButtonType.MIC, false)} + > + +
    + {/* Webcam Button */} +
    handleButtonHover(ButtonType.WEBCAM, true)} + onMouseLeave={() => handleButtonHover(ButtonType.WEBCAM, false)} + > + +
    + {/* Call End Button */} +
    handleButtonHover(ButtonType.CALL_END, true)} + onMouseLeave={() => handleButtonHover(ButtonType.CALL_END, false)} + > + +
    +
    + ) + ); +}; + +export default VideoMeetingControl; diff --git a/app/src/components/form/Selector.tsx b/app/src/components/form/Selector.tsx index f82d18267..cfc650985 100644 --- a/app/src/components/form/Selector.tsx +++ b/app/src/components/form/Selector.tsx @@ -1,70 +1,70 @@ -import React from 'react'; -import FormControl from '@mui/material/FormControl'; -import Select from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; - -type Props = { - items: []; - classes: { - configRow: any; - configType: any; - lightThemeFontColor: { color: String }; - formControl: any; - select: any; - selectInput: any; - darkThemeFontColor: { color: String }; - }; - isThemeLight: Boolean; - title: String; - selectValue: any; - handleChange: any; - name: String; -}; - -const FormSelector = (props): JSX.Element => { - const items = []; - let key = 1; - props.items.forEach((el) => { - items.push( - - {el.text} - - ); - key++; - }); - return ( -
    -
    -

    {props.title}

    -
    -
    - - - -
    -
    - ); -}; - -export default FormSelector; +import React from 'react'; +import FormControl from '@mui/material/FormControl'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; + +type Props = { + items: []; + classes: { + configRow: any; + configType: any; + lightThemeFontColor: { color: String }; + formControl: any; + select: any; + selectInput: any; + darkThemeFontColor: { color: String }; + }; + isThemeLight: Boolean; + title: String; + selectValue: any; + handleChange: any; + name: String; +}; + +const FormSelector = (props): JSX.Element => { + const items = []; + let key = 1; + props.items.forEach((el) => { + items.push( + + {el.text} + + ); + key++; + }); + return ( +
    +
    +

    {props.title}

    +
    +
    + + + +
    +
    + ); +}; + +export default FormSelector; diff --git a/app/src/components/left/ComponentDrag.tsx b/app/src/components/left/ComponentDrag.tsx index b3048fa40..5f5b6e973 100644 --- a/app/src/components/left/ComponentDrag.tsx +++ b/app/src/components/left/ComponentDrag.tsx @@ -1,69 +1,69 @@ -import ComponentPanelItem from '../right/ComponentPanelItem'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { RootState } from '../../redux/store'; -import makeStyles from '@mui/styles/makeStyles'; -import { useSelector } from 'react-redux'; - -const ComponentDrag = ({ isThemeLight }): JSX.Element => { - const classes = useStyles(); - const state = useSelector((store: RootState) => store.appState); - - const isFocus = (targetId: Number) => { - return state.canvasFocus.componentId === targetId ? true : false; - }; - - return ( -
    -
    -

    - {state.projectType === 'Next.js' || state.projectType === 'Gatsby.js' - ? 'Pages' - : 'Root Components'} -

    - - {state.components - .filter((comp) => state.rootComponents.includes(comp.id)) - .map((comp) => { - return ( - - ); - })} - -
    -
    - ); -}; - -const useStyles = makeStyles({ - panelWrapper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - flexGrow: 1, - overflow: 'auto' - }, - panelWrapperList: { - minHeight: '120px' - }, - lightThemeFontColor: { - color: '#fff' - }, - darkThemeFontColor: { - color: '#fff' - } -}); - -export default ComponentDrag; +import ComponentPanelItem from '../right/ComponentPanelItem'; +import Grid from '@mui/material/Grid'; +import React from 'react'; +import { RootState } from '../../redux/store'; +import makeStyles from '@mui/styles/makeStyles'; +import { useSelector } from 'react-redux'; + +const ComponentDrag = ({ isThemeLight }): JSX.Element => { + const classes = useStyles(); + const state = useSelector((store: RootState) => store.appState); + + const isFocus = (targetId: Number) => { + return state.canvasFocus.componentId === targetId ? true : false; + }; + + return ( +
    +
    +

    + {state.projectType === 'Next.js' || state.projectType === 'Gatsby.js' + ? 'Pages' + : 'Root Components'} +

    + + {state.components + .filter((comp) => state.rootComponents.includes(comp.id)) + .map((comp) => { + return ( + + ); + })} + +
    +
    + ); +}; + +const useStyles = makeStyles({ + panelWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flexGrow: 1, + overflow: 'auto' + }, + panelWrapperList: { + minHeight: '120px' + }, + lightThemeFontColor: { + color: '#fff' + }, + darkThemeFontColor: { + color: '#fff' + } +}); + +export default ComponentDrag; diff --git a/app/src/components/left/ComponentsContainer.tsx b/app/src/components/left/ComponentsContainer.tsx index 217aad4b0..eb0600f9f 100644 --- a/app/src/components/left/ComponentsContainer.tsx +++ b/app/src/components/left/ComponentsContainer.tsx @@ -1,61 +1,61 @@ -import ComponentPanelItem from '../right/ComponentPanelItem'; -import Grid from '@mui/material/Grid'; -import React from 'react'; -import { RootState } from '../../redux/store'; -import makeStyles from '@mui/styles/makeStyles'; -import { useSelector } from 'react-redux'; - -const ComponentsContainer = () => { - const classes = useStyles(); - const state = useSelector((store: RootState) => store.appState); - - const isFocus = (targetId: Number) => { - return state.canvasFocus.componentId === targetId ? true : false; - }; - return ( -
    -
    -
    -

    Reusable Components

    - - {state.components - .filter((comp) => !state.rootComponents.includes(comp.id)) - .map((comp) => { - return ( - - ); - })} - -
    -
    -
    - ); -}; - -const useStyles = makeStyles({ - panelWrapper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - flexGrow: 1, - overflow: 'auto' - }, - panelWrapperList: { - minHeight: '120px' - }, - lightThemeFontColor: { - color: '#fff' - }, - darkThemeFontColor: { - color: '#fff' - } -}); - -export default ComponentsContainer; +import ComponentPanelItem from '../right/ComponentPanelItem'; +import Grid from '@mui/material/Grid'; +import React from 'react'; +import { RootState } from '../../redux/store'; +import makeStyles from '@mui/styles/makeStyles'; +import { useSelector } from 'react-redux'; + +const ComponentsContainer = () => { + const classes = useStyles(); + const state = useSelector((store: RootState) => store.appState); + + const isFocus = (targetId: Number) => { + return state.canvasFocus.componentId === targetId ? true : false; + }; + return ( +
    +
    +
    +

    Reusable Components

    + + {state.components + .filter((comp) => !state.rootComponents.includes(comp.id)) + .map((comp) => { + return ( + + ); + })} + +
    +
    +
    + ); +}; + +const useStyles = makeStyles({ + panelWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flexGrow: 1, + overflow: 'auto' + }, + panelWrapperList: { + minHeight: '120px' + }, + lightThemeFontColor: { + color: '#fff' + }, + darkThemeFontColor: { + color: '#fff' + } +}); + +export default ComponentsContainer; diff --git a/app/src/components/left/ContentArea.tsx b/app/src/components/left/ContentArea.tsx index 8590dfac7..4f4538fba 100644 --- a/app/src/components/left/ContentArea.tsx +++ b/app/src/components/left/ContentArea.tsx @@ -1,49 +1,49 @@ -import { Box } from '@mui/material'; -import ComponentsContainer from './ComponentsContainer'; -import ElementsContainer from './ElementsContainer'; -import React from 'react'; -import RoomsContainer from './RoomsContainer'; -import ProfilePage from './ProfilePage'; -import Settings from './Settings'; - -interface ContentAreaProps { - activeTab: number | null; - isVisible: boolean; -} - -const TabPanel: React.FC<{ - children: React.ReactNode; - activeTab: number | null; - index: number; -}> = ({ children, activeTab, index }) => { - return ( - - ); -}; - -const panels = [ - , - , - , - , - -]; - -const ContentArea: React.FC = ({ activeTab, isVisible }) => { - return ( -
    -
    - {panels.map((panel, idx) => ( - - {panel} - - ))} -
    -
    - ); -}; - -export default ContentArea; +import { Box } from '@mui/material'; +import ComponentsContainer from './ComponentsContainer'; +import ElementsContainer from './ElementsContainer'; +import React from 'react'; +import RoomsContainer from './RoomsContainer'; +import ProfilePage from './ProfilePage'; +import Settings from './Settings'; + +interface ContentAreaProps { + activeTab: number | null; + isVisible: boolean; +} + +const TabPanel: React.FC<{ + children: React.ReactNode; + activeTab: number | null; + index: number; +}> = ({ children, activeTab, index }) => { + return ( + + ); +}; + +const panels = [ + , + , + , + , + +]; + +const ContentArea: React.FC = ({ activeTab, isVisible }) => { + return ( +
    +
    + {panels.map((panel, idx) => ( + + {panel} + + ))} +
    +
    + ); +}; + +export default ContentArea; diff --git a/app/src/components/left/DragDropPanel.tsx b/app/src/components/left/DragDropPanel.tsx index 33f371c73..97aa88352 100644 --- a/app/src/components/left/DragDropPanel.tsx +++ b/app/src/components/left/DragDropPanel.tsx @@ -1,128 +1,128 @@ -import { useDispatch, useSelector } from 'react-redux'; -import Grid from '@mui/material/Grid'; -import HTMLItem from './HTMLItem'; -import React from 'react'; -import { RootState } from '../../redux/store'; -import { deleteElement } from '../../redux/reducers/slice/appStateSlice'; -import { emitEvent } from '../../helperFunctions/socket'; - -/* -DESCRIPTION: This is the top half of the left panel, starting from the 'HTML - Elements' header. The boxes containing each HTML element are rendered in - HTMLItem, which itself is rendered by this component. - -Central state contains all available HTML elements (stored in the HTMLTypes property). - The data for HTMLTypes is stored in HTMLTypes.tsx and is added to central state in - initialState.tsx. - -Hook state: - -tag: -*/ -// Extracted the drag and drop functionality from HTMLPanel to make a modular component that can hang wherever the future designers may choose. -const DragDropPanel = (props): JSX.Element => { - const dispatch = useDispatch(); - - const state = useSelector((store: RootState) => store.appState); // current state - const contextParam = useSelector((store: RootState) => store.contextSlice); // current contextParam - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode - - // function for delete element - const handleDelete = (id: number): void => { - dispatch(deleteElement({ id: id, contextParam: contextParam })); - if (roomCode) { - // send delete element to server to broadcast to all users - emitEvent('deleteElementAction', roomCode, { - id, - contextParam - }); - } - }; - - // filter out separator so that it will not appear on the html panel - const htmlTypesToRender = state.HTMLTypes.filter( - (type) => type.name !== 'separator' - ); - return ( -
    -
    -

    HTML Elements

    - - {htmlTypesToRender.map((option) => { - if ( - !['Switch', 'LinkTo', 'LinkHref', 'Image', 'Route'].includes( - option.name - ) - ) { - return ( - - ); - } - })} - - - {state.projectType === 'Classic React' ? ( -

    React Router

    - ) : null} - - {htmlTypesToRender.map((option) => { - if ( - (option.name === 'Switch' || - option.name === 'LinkTo' || - option.name === 'Route') && - state.projectType === 'Classic React' - ) { - return ( - - ); - } - })} - - - {/* Next.js */} - {state.projectType === 'Next.js' ? ( -

    Next.js

    - ) : null} - {htmlTypesToRender.map((option) => { - if ( - option.framework === 'nextjs' && - state.projectType === 'Next.js' - ) { - return ( - - ); - } - })} -
    -
    - ); -}; - -export default DragDropPanel; +import { useDispatch, useSelector } from 'react-redux'; +import Grid from '@mui/material/Grid'; +import HTMLItem from './HTMLItem'; +import React from 'react'; +import { RootState } from '../../redux/store'; +import { deleteElement } from '../../redux/reducers/slice/appStateSlice'; +import { emitEvent } from '../../helperFunctions/socket'; + +/* +DESCRIPTION: This is the top half of the left panel, starting from the 'HTML + Elements' header. The boxes containing each HTML element are rendered in + HTMLItem, which itself is rendered by this component. + +Central state contains all available HTML elements (stored in the HTMLTypes property). + The data for HTMLTypes is stored in HTMLTypes.tsx and is added to central state in + initialState.tsx. + +Hook state: + -tag: +*/ +// Extracted the drag and drop functionality from HTMLPanel to make a modular component that can hang wherever the future designers may choose. +const DragDropPanel = (props): JSX.Element => { + const dispatch = useDispatch(); + + const state = useSelector((store: RootState) => store.appState); // current state + const contextParam = useSelector((store: RootState) => store.contextSlice); // current contextParam + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode + + // function for delete element + const handleDelete = (id: number): void => { + dispatch(deleteElement({ id: id, contextParam: contextParam })); + if (roomCode) { + // send delete element to server to broadcast to all users + emitEvent('deleteElementAction', roomCode, { + id, + contextParam + }); + } + }; + + // filter out separator so that it will not appear on the html panel + const htmlTypesToRender = state.HTMLTypes.filter( + (type) => type.name !== 'separator' + ); + return ( +
    +
    +

    HTML Elements

    + + {htmlTypesToRender.map((option) => { + if ( + !['Switch', 'LinkTo', 'LinkHref', 'Image', 'Route'].includes( + option.name + ) + ) { + return ( + + ); + } + })} + + + {state.projectType === 'Classic React' ? ( +

    React Router

    + ) : null} + + {htmlTypesToRender.map((option) => { + if ( + (option.name === 'Switch' || + option.name === 'LinkTo' || + option.name === 'Route') && + state.projectType === 'Classic React' + ) { + return ( + + ); + } + })} + + + {/* Next.js */} + {state.projectType === 'Next.js' ? ( +

    Next.js

    + ) : null} + {htmlTypesToRender.map((option) => { + if ( + option.framework === 'nextjs' && + state.projectType === 'Next.js' + ) { + return ( + + ); + } + })} +
    +
    + ); +}; + +export default DragDropPanel; diff --git a/app/src/components/left/ElementsContainer.tsx b/app/src/components/left/ElementsContainer.tsx index bff6d195b..6d95bcf1f 100644 --- a/app/src/components/left/ElementsContainer.tsx +++ b/app/src/components/left/ElementsContainer.tsx @@ -1,65 +1,65 @@ -import { Box } from '@mui/material'; -import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import ComponentDrag from './ComponentDrag'; -import DragDropPanel from './DragDropPanel'; - -import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; -import { emitEvent } from '../../helperFunctions/socket'; - -// Left-hand portion of the app, where predefined component options are displayed -const ElementsContainer = (props): JSX.Element => { - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - - const handleDelete = () => { - dispatch(deleteChild({ id: {}, contextParam: contextParam })); - if (roomCode) { - emitEvent('deleteChildAction', roomCode, { - id: {}, - contextParam: contextParam - }); - } - }; - - const keyBindedFunc = useCallback((e) => { - if ( - e.key === 'Backspace' && - e.target.tagName !== 'TEXTAREA' && - e.target.tagName !== 'INPUT' - ) - handleDelete(); - }, []); - - useEffect(() => { - document.addEventListener('keydown', keyBindedFunc); - return () => { - document.removeEventListener('keydown', keyBindedFunc); - }; - }, []); - - return ( - - {' '} - -
    - -
    -
    - ); -}; - -export default ElementsContainer; +import { Box } from '@mui/material'; +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import ComponentDrag from './ComponentDrag'; +import DragDropPanel from './DragDropPanel'; + +import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; + +// Left-hand portion of the app, where predefined component options are displayed +const ElementsContainer = (props): JSX.Element => { + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + + const handleDelete = () => { + dispatch(deleteChild({ id: {}, contextParam: contextParam })); + if (roomCode) { + emitEvent('deleteChildAction', roomCode, { + id: {}, + contextParam: contextParam + }); + } + }; + + const keyBindedFunc = useCallback((e) => { + if ( + e.key === 'Backspace' && + e.target.tagName !== 'TEXTAREA' && + e.target.tagName !== 'INPUT' + ) + handleDelete(); + }, []); + + useEffect(() => { + document.addEventListener('keydown', keyBindedFunc); + return () => { + document.removeEventListener('keydown', keyBindedFunc); + }; + }, []); + + return ( + + {' '} + +
    + +
    +
    + ); +}; + +export default ElementsContainer; diff --git a/app/src/components/left/HTMLItem.tsx b/app/src/components/left/HTMLItem.tsx index d5948a5c7..17e73817e 100644 --- a/app/src/components/left/HTMLItem.tsx +++ b/app/src/components/left/HTMLItem.tsx @@ -1,186 +1,186 @@ -import React, { useState } from 'react'; -import Grid from '@mui/material/Grid'; -import { ItemTypes } from '../../constants/ItemTypes'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import createModal from '../right/createModal'; -import makeStyles from '@mui/styles/makeStyles'; -import { useDrag } from 'react-dnd'; -import CodeIcon from '@mui/icons-material/Code'; -import * as Icons from '@mui/icons-material'; -import { useDispatch, useSelector } from 'react-redux'; -import { addChild } from '../../redux/reducers/slice/appStateSlice'; -import { emitEvent } from '../../helperFunctions/socket'; - -const useStyles = makeStyles({ - HTMLPanelItem: { - height: 'auto', - width: 'auto', - fontSize: 'medium', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-evenly', - textAlign: 'center', - cursor: 'grab' - }, - lightThemeFontColor: { - color: '#8F8F8F' - }, - darkThemeFontColor: { - color: '#8F8F8F' - } -}); - -const HTMLItem: React.FC<{ - name: string; - id: number; - icon: any; - handleDelete: (id: number) => void; -}> = ({ name, id, icon, handleDelete }) => { - const IconComponent = Icons[icon]; - - - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode - - - const classes = useStyles(); - const [modal, setModal] = useState(null); - const [{ isDragging }, drag] = useDrag({ - item: { - type: ItemTypes.INSTANCE, - newInstance: true, - instanceType: 'HTML Element', - name, - icon, - instanceTypeId: id - }, - collect: (monitor: any) => ({ - isDragging: !!monitor.isDragging() - }) - }); - - const closeModal = () => setModal(null); - const deleteAllInstances = (deleteID: number) => { - const children = ( - - handleDelete(deleteID)} - style={{ - border: '1px solid #C6C6C6', - marginBottom: '2%', - marginTop: '5%' - }} - > - - - - - - - ); - setModal( - createModal({ - closeModal, - children, - message: - 'Deleting this element will delete all instances of this element within the application.\nDo you still wish to proceed?', - primBtnLabel: null, - primBtnAction: null, - secBtnAction: null, - secBtnLabel: null, - open: true - }) - ); - }; - - - const dispatch = useDispatch(); - - const handleClick = () => { - const childData = { - type: 'HTML Element', - typeId: id, - childId: null, - contextParam: { - allContext: [] - } - }; - - dispatch(addChild(childData)); - if (roomCode) { - // Emit 'addChildAction' event to the server - emitEvent('addChildAction', roomCode, childData); - } - }; - - // updated the id's to reflect the new element types input and label - - return ( - - {id <= 20 && ( -
    { - handleClick(); - }} - > - {typeof IconComponent !== 'undefined' && ( - - )} - {name} -
    - )} - - {id > 20 && ( -
    { - handleClick(); - }} - > - {typeof CodeIcon !== 'undefined' && ( - - )} - {name} - -
    - )} - {modal} -
    - ); -}; - -export default HTMLItem; - +import React, { useState } from 'react'; +import Grid from '@mui/material/Grid'; +import { ItemTypes } from '../../constants/ItemTypes'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import createModal from '../right/createModal'; +import makeStyles from '@mui/styles/makeStyles'; +import { useDrag } from 'react-dnd'; +import CodeIcon from '@mui/icons-material/Code'; +import * as Icons from '@mui/icons-material'; +import { useDispatch, useSelector } from 'react-redux'; +import { addChild } from '../../redux/reducers/slice/appStateSlice'; +import { emitEvent } from '../../helperFunctions/socket'; + +const useStyles = makeStyles({ + HTMLPanelItem: { + height: 'auto', + width: 'auto', + fontSize: 'medium', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-evenly', + textAlign: 'center', + cursor: 'grab' + }, + lightThemeFontColor: { + color: '#8F8F8F' + }, + darkThemeFontColor: { + color: '#8F8F8F' + } +}); + +const HTMLItem: React.FC<{ + name: string; + id: number; + icon: any; + handleDelete: (id: number) => void; +}> = ({ name, id, icon, handleDelete }) => { + const IconComponent = Icons[icon]; + + + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode + + + const classes = useStyles(); + const [modal, setModal] = useState(null); + const [{ isDragging }, drag] = useDrag({ + item: { + type: ItemTypes.INSTANCE, + newInstance: true, + instanceType: 'HTML Element', + name, + icon, + instanceTypeId: id + }, + collect: (monitor: any) => ({ + isDragging: !!monitor.isDragging() + }) + }); + + const closeModal = () => setModal(null); + const deleteAllInstances = (deleteID: number) => { + const children = ( + + handleDelete(deleteID)} + style={{ + border: '1px solid #C6C6C6', + marginBottom: '2%', + marginTop: '5%' + }} + > + + + + + + + ); + setModal( + createModal({ + closeModal, + children, + message: + 'Deleting this element will delete all instances of this element within the application.\nDo you still wish to proceed?', + primBtnLabel: null, + primBtnAction: null, + secBtnAction: null, + secBtnLabel: null, + open: true + }) + ); + }; + + + const dispatch = useDispatch(); + + const handleClick = () => { + const childData = { + type: 'HTML Element', + typeId: id, + childId: null, + contextParam: { + allContext: [] + } + }; + + dispatch(addChild(childData)); + if (roomCode) { + // Emit 'addChildAction' event to the server + emitEvent('addChildAction', roomCode, childData); + } + }; + + // updated the id's to reflect the new element types input and label + + return ( + + {id <= 20 && ( +
    { + handleClick(); + }} + > + {typeof IconComponent !== 'undefined' && ( + + )} + {name} +
    + )} + + {id > 20 && ( +
    { + handleClick(); + }} + > + {typeof CodeIcon !== 'undefined' && ( + + )} + {name} + +
    + )} + {modal} +
    + ); +}; + +export default HTMLItem; + diff --git a/app/src/components/left/HTMLPanel.tsx b/app/src/components/left/HTMLPanel.tsx index f1389214a..611d1abf1 100644 --- a/app/src/components/left/HTMLPanel.tsx +++ b/app/src/components/left/HTMLPanel.tsx @@ -1,375 +1,375 @@ -import { Button, InputLabel } from '@mui/material'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { RootState } from '../../redux/store'; -import TextField from '@mui/material/TextField'; -import { addElement } from '../../redux/reducers/slice/appStateSlice'; -import makeStyles from '@mui/styles/makeStyles'; -import MuiAlert, { AlertProps } from '@mui/material/Alert'; -import Snackbar from '@mui/material/Snackbar'; -import { emitEvent } from '../../helperFunctions/socket'; - -/* -DESCRIPTION: This is the bottom half of the left panel, starting from the 'HTML - Elements' header. The boxes containing each HTML element are rendered in - HTMLItem, which itself is rendered by this component. - - !!! TO NAME HTML ELEMENTS in the LEFT panel !!! - -Central state contains all available HTML elements (stored in the HTMLTypes property). - The data for HTMLTypes is stored in HTMLTypes.tsx and is added to central state in - initialState.tsx. - -Hook state: - -tag: -*/ - -const HTMLPanel = (props): JSX.Element => { - const classes = useStyles(); - const [tag, setTag] = useState(''); - const [name, setName] = useState(''); - const [errorMsg, setErrorMsg] = useState(''); - const [errorStatus, setErrorStatus] = useState(false); - const [alertOpen, setAlertOpen] = React.useState(false); - const state = useSelector((store: RootState) => store.appState); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const currentID = useSelector( - (store: RootState) => store.appState.customElementId - ); - - const dispatch = useDispatch(); - - const handleTagChange = (e: React.ChangeEvent) => { - resetError(); - setTag(e.target.value); - }; - - const handleNameChange = (e: React.ChangeEvent) => { - resetError(); - setName(e.target.value); - }; - - const checkNameDupe = (inputName: String): boolean => { - let checkList = state.HTMLTypes.slice(); - - // checks to see if inputted comp name already exists - let dupe = false; - checkList.forEach((HTMLTag) => { - if ( - HTMLTag.name.toLowerCase() === inputName.toLowerCase() || - HTMLTag.tag.toLowerCase() === inputName.toLowerCase() - ) { - dupe = true; - } - }); - return dupe; - }; - - const triggerError = (type: String) => { - setErrorStatus(true); - if (type === 'empty') { - setErrorMsg('* Input cannot be blank. *'); - } else if (type === 'dupe') { - setErrorMsg('* Input already exists. *'); - } else if (type === 'letters') { - setErrorMsg('* Input must start with a letter. *'); - } else if (type === 'symbolsDetected') { - setErrorMsg('* Input must not contain symbols. *'); - } else if (type === 'length') { - setErrorMsg('* Input cannot exceed 10 characters. *'); - } - }; - - const resetError = () => { - setErrorStatus(false); - }; - - const createOption = (inputTag: String, inputName: String) => { - // format name so first letter is capitalized and there are no whitespaces - let inputNameClean = inputName.replace(/\s+/g, ''); - const formattedName = - inputNameClean.charAt(0).toUpperCase() + inputNameClean.slice(1); - // add new component to state - const newElement = { - id: currentID, - tag: inputTag, - name: formattedName, - style: {}, - placeHolderShort: name, - placeHolderLong: '', - icon: null - }; - - dispatch(addElement(newElement)); - - if (roomCode) { - emitEvent('addElementAction', roomCode, newElement); - } - - // setCurrentID(currentID + 1); - setTag(''); - setName(''); - }; - - const alphanumeric = (input: string): boolean => { - let letterNumber = /^[0-9a-zA-Z]+$/; - if (input.match(letterNumber)) return true; - return false; - }; - - const handleSubmit = (e) => { - e.preventDefault(); - - if (tag.trim() === '' || name.trim() === '') { - triggerError('empty'); - return; - } else if ( - !tag.charAt(0).match(/[a-zA-Z]/) || - !name.charAt(0).match(/[a-zA-Z]/) - ) { - triggerError('letters'); - return; - } else if (!alphanumeric(tag) || !alphanumeric(name)) { - triggerError('symbolsDetected'); - return; - } else if (checkNameDupe(tag) || checkNameDupe(name)) { - triggerError('dupe'); - return; - } else if (name.length > 10) { - triggerError('length'); - return; - } - createOption(tag, name); - resetError(); - }; - - const handleCreateElement = useCallback((e) => { - if ( - e.key === 'Enter' && - e.target.tagName !== 'TEXTAREA' && - e.target.id !== 'filled-hidden-label-small' - ) { - e.preventDefault(); - document.getElementById('submitButton').click(); - } - }, []); - - useEffect(() => { - document.addEventListener('keydown', handleCreateElement); - return () => { - document.removeEventListener('keydown', handleCreateElement); - }; - }, []); - - const handleAlertOpen = () => { - setAlertOpen(true); - }; - - const handleAlertClose = ( - event: React.SyntheticEvent | Event, - reason?: string - ) => { - if (reason === 'clickaway') { - return; - } - setAlertOpen(false); - }; - - const Alert = React.forwardRef(function Alert( - props, - ref - ) { - return ; - }); - - return ( - <> -
    -
    -
    -
    -

    New HTML Tag

    - - Tag - - - - {(!tag.charAt(0).match(/[A-Za-z]/) || - !alphanumeric(tag) || - tag.trim() === '' || - checkNameDupe(tag)) && ( - - {errorMsg} - - )} - -

    - - Element Name - - - {(!name.charAt(0).match(/[A-Za-z]/) || - !alphanumeric(name) || - name.trim() === '' || - name.length > 10 || - checkNameDupe(name)) && ( - - {errorMsg} - - )} -

    - - -
    -
    -
    - <> - - - HTML Tag Created! - - - - - ); -}; - -const useStyles = makeStyles({ - inputField: { - marginTop: '10px', - borderRadius: '5px', - whiteSpace: 'nowrap', - overflowX: 'hidden', - textOverflow: 'ellipsis', - backgroundColor: 'rgba(255,255,255,0.15)', - margin: '0px 0px 0px 10px', - width: '140px', - height: '30px' - }, - inputWrapper: { - textAlign: 'center', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-evenly', - marginBottom: '15px', - width: '100%' - }, - addComponentWrapper: { - width: '100%' - }, - input: { - width: '500px', - whiteSpace: 'nowrap', - overflowX: 'hidden', - textOverflow: 'ellipsis', - margin: '0px 0px 0px 0px', - alignSelf: 'center' - }, - inputLabel: { - fontSize: '1em', - marginLeft: '10px' - }, - addElementButton: { - height: '50px', - width: '150px', - fontFamily: 'Roboto, Raleway, sans-serif', - fontSize: '15.5px', - textAlign: 'center', - transition: '0.3s', - borderRadius: '10px', - alignSelf: 'end', - border: '1px solid #0671E3' - }, - lightThemeFontColor: { - color: 'white', - '& .MuiInputBase-root': { - color: 'rgba (0, 0, 0, 0.54)' - } - }, - darkThemeFontColor: { - color: '#ffffff', - '& .MuiInputBase-root': { - color: '#fff' - } - }, - errorMessage: { - display: 'flex', - alignSelf: 'center', - fontSize: '11px', - marginTop: '10px', - width: '150px' - }, - errorMessageLight: { - color: '#6B6B6B' - }, - errorMessageDark: { - color: 'white' - } -}); - -export default HTMLPanel; +import { Button, InputLabel } from '@mui/material'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { RootState } from '../../redux/store'; +import TextField from '@mui/material/TextField'; +import { addElement } from '../../redux/reducers/slice/appStateSlice'; +import makeStyles from '@mui/styles/makeStyles'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import Snackbar from '@mui/material/Snackbar'; +import { emitEvent } from '../../helperFunctions/socket'; + +/* +DESCRIPTION: This is the bottom half of the left panel, starting from the 'HTML + Elements' header. The boxes containing each HTML element are rendered in + HTMLItem, which itself is rendered by this component. + + !!! TO NAME HTML ELEMENTS in the LEFT panel !!! + +Central state contains all available HTML elements (stored in the HTMLTypes property). + The data for HTMLTypes is stored in HTMLTypes.tsx and is added to central state in + initialState.tsx. + +Hook state: + -tag: +*/ + +const HTMLPanel = (props): JSX.Element => { + const classes = useStyles(); + const [tag, setTag] = useState(''); + const [name, setName] = useState(''); + const [errorMsg, setErrorMsg] = useState(''); + const [errorStatus, setErrorStatus] = useState(false); + const [alertOpen, setAlertOpen] = React.useState(false); + const state = useSelector((store: RootState) => store.appState); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const currentID = useSelector( + (store: RootState) => store.appState.customElementId + ); + + const dispatch = useDispatch(); + + const handleTagChange = (e: React.ChangeEvent) => { + resetError(); + setTag(e.target.value); + }; + + const handleNameChange = (e: React.ChangeEvent) => { + resetError(); + setName(e.target.value); + }; + + const checkNameDupe = (inputName: String): boolean => { + let checkList = state.HTMLTypes.slice(); + + // checks to see if inputted comp name already exists + let dupe = false; + checkList.forEach((HTMLTag) => { + if ( + HTMLTag.name.toLowerCase() === inputName.toLowerCase() || + HTMLTag.tag.toLowerCase() === inputName.toLowerCase() + ) { + dupe = true; + } + }); + return dupe; + }; + + const triggerError = (type: String) => { + setErrorStatus(true); + if (type === 'empty') { + setErrorMsg('* Input cannot be blank. *'); + } else if (type === 'dupe') { + setErrorMsg('* Input already exists. *'); + } else if (type === 'letters') { + setErrorMsg('* Input must start with a letter. *'); + } else if (type === 'symbolsDetected') { + setErrorMsg('* Input must not contain symbols. *'); + } else if (type === 'length') { + setErrorMsg('* Input cannot exceed 10 characters. *'); + } + }; + + const resetError = () => { + setErrorStatus(false); + }; + + const createOption = (inputTag: String, inputName: String) => { + // format name so first letter is capitalized and there are no whitespaces + let inputNameClean = inputName.replace(/\s+/g, ''); + const formattedName = + inputNameClean.charAt(0).toUpperCase() + inputNameClean.slice(1); + // add new component to state + const newElement = { + id: currentID, + tag: inputTag, + name: formattedName, + style: {}, + placeHolderShort: name, + placeHolderLong: '', + icon: null + }; + + dispatch(addElement(newElement)); + + if (roomCode) { + emitEvent('addElementAction', roomCode, newElement); + } + + // setCurrentID(currentID + 1); + setTag(''); + setName(''); + }; + + const alphanumeric = (input: string): boolean => { + let letterNumber = /^[0-9a-zA-Z]+$/; + if (input.match(letterNumber)) return true; + return false; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (tag.trim() === '' || name.trim() === '') { + triggerError('empty'); + return; + } else if ( + !tag.charAt(0).match(/[a-zA-Z]/) || + !name.charAt(0).match(/[a-zA-Z]/) + ) { + triggerError('letters'); + return; + } else if (!alphanumeric(tag) || !alphanumeric(name)) { + triggerError('symbolsDetected'); + return; + } else if (checkNameDupe(tag) || checkNameDupe(name)) { + triggerError('dupe'); + return; + } else if (name.length > 10) { + triggerError('length'); + return; + } + createOption(tag, name); + resetError(); + }; + + const handleCreateElement = useCallback((e) => { + if ( + e.key === 'Enter' && + e.target.tagName !== 'TEXTAREA' && + e.target.id !== 'filled-hidden-label-small' + ) { + e.preventDefault(); + document.getElementById('submitButton').click(); + } + }, []); + + useEffect(() => { + document.addEventListener('keydown', handleCreateElement); + return () => { + document.removeEventListener('keydown', handleCreateElement); + }; + }, []); + + const handleAlertOpen = () => { + setAlertOpen(true); + }; + + const handleAlertClose = ( + event: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === 'clickaway') { + return; + } + setAlertOpen(false); + }; + + const Alert = React.forwardRef(function Alert( + props, + ref + ) { + return ; + }); + + return ( + <> +
    +
    +
    +
    +

    New HTML Tag

    + + Tag + + + + {(!tag.charAt(0).match(/[A-Za-z]/) || + !alphanumeric(tag) || + tag.trim() === '' || + checkNameDupe(tag)) && ( + + {errorMsg} + + )} + +

    + + Element Name + + + {(!name.charAt(0).match(/[A-Za-z]/) || + !alphanumeric(name) || + name.trim() === '' || + name.length > 10 || + checkNameDupe(name)) && ( + + {errorMsg} + + )} +

    + + +
    +
    +
    + <> + + + HTML Tag Created! + + + + + ); +}; + +const useStyles = makeStyles({ + inputField: { + marginTop: '10px', + borderRadius: '5px', + whiteSpace: 'nowrap', + overflowX: 'hidden', + textOverflow: 'ellipsis', + backgroundColor: 'rgba(255,255,255,0.15)', + margin: '0px 0px 0px 10px', + width: '140px', + height: '30px' + }, + inputWrapper: { + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-evenly', + marginBottom: '15px', + width: '100%' + }, + addComponentWrapper: { + width: '100%' + }, + input: { + width: '500px', + whiteSpace: 'nowrap', + overflowX: 'hidden', + textOverflow: 'ellipsis', + margin: '0px 0px 0px 0px', + alignSelf: 'center' + }, + inputLabel: { + fontSize: '1em', + marginLeft: '10px' + }, + addElementButton: { + height: '50px', + width: '150px', + fontFamily: 'Roboto, Raleway, sans-serif', + fontSize: '15.5px', + textAlign: 'center', + transition: '0.3s', + borderRadius: '10px', + alignSelf: 'end', + border: '1px solid #0671E3' + }, + lightThemeFontColor: { + color: 'white', + '& .MuiInputBase-root': { + color: 'rgba (0, 0, 0, 0.54)' + } + }, + darkThemeFontColor: { + color: '#ffffff', + '& .MuiInputBase-root': { + color: '#fff' + } + }, + errorMessage: { + display: 'flex', + alignSelf: 'center', + fontSize: '11px', + marginTop: '10px', + width: '150px' + }, + errorMessageLight: { + color: '#6B6B6B' + }, + errorMessageDark: { + color: 'white' + } +}); + +export default HTMLPanel; diff --git a/app/src/components/left/ProfilePage.tsx b/app/src/components/left/ProfilePage.tsx index 173ca0e2f..013c89a23 100644 --- a/app/src/components/left/ProfilePage.tsx +++ b/app/src/components/left/ProfilePage.tsx @@ -1,161 +1,161 @@ -import React, { useState, useEffect } from 'react'; -import { RootState } from '../../redux/store'; -import makeStyles from '@mui/styles/makeStyles'; -import { useSelector } from 'react-redux'; -import { - Box, - Card, - CardActions, - CardContent, - Button, - Typography, - Divider -} from '@mui/material'; - -const bull = ( - - • - -); - -const ProfilePage = () => { - const classes = useStyles(); - const [username, setUsername] = useState(''); - const [email, setEmail] = useState(''); - - useEffect(() => { - const storedUsername = window.localStorage.getItem('username'); - const storedEmail = window.localStorage.getItem('email'); - console.log(localStorage); - - if (storedUsername) { - setUsername(storedUsername); - } - - if (storedUsername) { - setEmail(storedEmail); - } - }, []); - - return ( - - - - Hello, - - - {username ? username : 'Guest'} - - - Welcome to Reactype! - - {email ? ( - - Email: -
    - {email} -
    - ) : null} -
    - - - - - - Apps - - - Create a web app or a native mobile app to build a custom internal - tool for your business. - - - - - - - - Resources - - - Securely connect your data and display it inside of Reactype apps. - - - - - - - - - - - Database - - - Easily store data in a free SQL database and power your Reactype app. - - - - - - - -
    - ); -}; - -const useStyles = makeStyles({ - panelWrapper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - flexGrow: 1, - overflow: 'auto' - }, - panelWrapperList: { - minHeight: '120px' - }, - lightThemeFontColor: { - color: '#fff' - }, - darkThemeFontColor: { - color: '#fff' - } -}); - -export default ProfilePage; +import React, { useState, useEffect } from 'react'; +import { RootState } from '../../redux/store'; +import makeStyles from '@mui/styles/makeStyles'; +import { useSelector } from 'react-redux'; +import { + Box, + Card, + CardActions, + CardContent, + Button, + Typography, + Divider +} from '@mui/material'; + +const bull = ( + + • + +); + +const ProfilePage = () => { + const classes = useStyles(); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + + useEffect(() => { + const storedUsername = window.localStorage.getItem('username'); + const storedEmail = window.localStorage.getItem('email'); + console.log(localStorage); + + if (storedUsername) { + setUsername(storedUsername); + } + + if (storedUsername) { + setEmail(storedEmail); + } + }, []); + + return ( + + + + Hello, + + + {username ? username : 'Guest'} + + + Welcome to Reactype! + + {email ? ( + + Email: +
    + {email} +
    + ) : null} +
    + + + + + + Apps + + + Create a web app or a native mobile app to build a custom internal + tool for your business. + + + + + + + + Resources + + + Securely connect your data and display it inside of Reactype apps. + + + + + + + + + + + Database + + + Easily store data in a free SQL database and power your Reactype app. + + + + + + + +
    + ); +}; + +const useStyles = makeStyles({ + panelWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flexGrow: 1, + overflow: 'auto' + }, + panelWrapperList: { + minHeight: '120px' + }, + lightThemeFontColor: { + color: '#fff' + }, + darkThemeFontColor: { + color: '#fff' + } +}); + +export default ProfilePage; diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index 6a562fd8f..944acceba 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -1,595 +1,595 @@ -import { useState } from 'react'; -import { Stack, Typography } from '@mui/material'; -import { useDispatch, useSelector } from 'react-redux'; -import Box from '@mui/material/Box'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import Button from '@mui/material/Button'; -import { RootState } from '../../redux/store'; -import TextField from '@mui/material/TextField'; -import { BottomPanelObj } from '../../interfaces/Interfaces'; -import { - allCooperativeState, - addChild, - changeFocus, - deleteChild, - changePosition, - resetState, - updateStateUsed, - updateUseContext, - updateCss, - updateAttributes, - updateEvents, - addComponent, - addElement, - addState, - deleteState, - addPassedInProps, - deletePassedInProps, - deleteElement, - resetAllState, - updateStylesheet -} from '../../redux/reducers/slice/appStateSlice'; -import { - addContext, - deleteContext, - addContextValues -} from '../../redux/reducers/slice/contextReducer'; -import { - setRoomCode, - setUserName, - setUserJoinCollabRoom, - setUserList, - setMeetingId, - setMessages, - setEmptyMessages, - setPassword, - setUseMic, - setUseWebcam -} from '../../redux/reducers/slice/roomSlice'; -import { codePreviewCooperative } from '../../redux/reducers/slice/codePreviewSlice'; -import { cooperativeStyle } from '../../redux/reducers/slice/styleSlice'; -import store from '../../redux/store'; -import { initializeSocket, getSocket } from '../../helperFunctions/socket'; -import { - AddContextPayload, - AddContextValuesPayload, - DeleteContextPayload, - addComponentToContext -} from '../../../src/redux/reducers/slice/contextReducer'; - -const RoomsContainer = () => { - const [isJoinCallabRoom, setIsJoinCollabRoom] = useState(false); - const [joinedPasswordAttempt, setJoinedPasswordAttempt] = useState(''); - const [isPasswordAttemptIncorrect, setIsPasswordAttemptIncorrect] = - useState(true); - const [isCollabRoomTaken, setIsCollabRoomTaken] = useState(false); - const [isRoomAvailable, setIsRoomAvailable] = useState(true); - - const dispatch = useDispatch(); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const userName = useSelector((store: RootState) => store.roomSlice.userName); - const userList = useSelector((store: RootState) => store.roomSlice.userList); - const roomPassword = useSelector( - (store: RootState) => store.roomSlice.password - ); - - - const userJoinCollabRoom = useSelector( - (store: RootState) => store.roomSlice.userJoinCollabRoom - ); - - const messages = useSelector((store: RootState) => store.roomSlice.messages); - - const initSocketConnection = ( - roomCode: string, - roomPassword: string, - method: string - ) => { - // helper function to create socket connection - initializeSocket(); - // assign socket to result of helper function to return socket created - const socket = getSocket(); - // if socket was created correctly and exists - if (socket) { - //run everytime when a client connects to server - socket.on('connect', () => { - socket.emit( - 'creating a room', - userName, - roomCode, - roomPassword, - method - ); - }); - - socket.on('wrong password', () => { - setIsPasswordAttemptIncorrect(false); - }); - - socket.on('correct password', () => { - setIsPasswordAttemptIncorrect(true); - addNewUserToCollabRoom(); - }); - - socket.on('user created a new room', () => { - addNewUserToCollabRoom(); - }); - - socket.on('room is already taken', () => { - setIsCollabRoomTaken(true); - }); - - socket.on('room does not exist', () => { - setIsRoomAvailable(false); - - }); - //If you are the host: send current state to server when a new user joins - socket.on('requesting state from host', (callback) => { - const newState = store.getState(); //pull the current state - callback(newState); //send it to backend server - }); - - //If you are the new user: receive the state from the host - socket.on('server emitting state from host', (state, callback) => { - //dispatching new state to change user current state - // console.log('state received by new join:', state); - store.dispatch(allCooperativeState(state.appState)); - store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); - store.dispatch(cooperativeStyle(state.styleSlice)); - callback({ status: 'confirmed' }); - }); - - // update user list when there's a change: new join or leave the room - socket.on('update room information', (messageData) => { - //console.log('user list received from server'); - if (messageData.userList) dispatch(setUserList(messageData.userList)); - if (messageData.meetingId) - dispatch(setMeetingId(messageData.meetingId)); - }); - - socket.on('new chat message', (messageData) => { - if ( - messages.length === 0 || - JSON.stringify(messageData) !== JSON.stringify(messages[-1]) - ) { - dispatch(setMessages(messageData)); - } - }); - - // dispatch add child to local state when element has been added by another user - socket.on('child data from server', (childData: object) => { - // console.log('child data received by users', childData); - store.dispatch(addChild(childData)); - }); - - // dispatch changeFocus to local state when another user has changed focus by selecting element on canvas - socket.on('focus data from server', (focusData: object) => { - // console.log('focus data received from server', focusData); - store.dispatch(changeFocus(focusData)); - }); - - // dispatch deleteChild to local state when another user has deleted an element - socket.on('delete data from server', (deleteData: object) => { - // console.log('delete data received from server', deleteData); - store.dispatch(deleteChild(deleteData)); - }); - - // dispatch delete element to local state when another user has deleted an element - socket.on( - 'delete element data from server', - (deleteElementData: object) => { - // console.log('delete element data received from server', deleteElementData); - store.dispatch(deleteElement(deleteElementData)); - } - ); - - // dispatch clear canvas action to local state when the host of the room has clear canvas - socket.on('clear canvas from server', () => { - store.dispatch(resetAllState()); - }); - - // dispatch all updates to local state when another user has saved from Bottom Panel - socket.on('update data from server', (updateData: BottomPanelObj) => { - // console.log('update data received from server', updateData); - store.dispatch( - updateStateUsed({ - stateUsedObj: updateData.stateUsedObj, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateUseContext({ - useContextObj: updateData.useContextObj, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateCss({ - style: updateData.style, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateAttributes({ - attributes: updateData.attributes, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateEvents({ - events: updateData.events, - contextParam: updateData.contextParam - }) - ); - }); - - // dispatch update style in local state when CSS panel is updated on their side - socket.on('update css data from server', (cssData: object) => { - // console.log('CSS data received from server', cssData); - store.dispatch(updateStylesheet(cssData)); - }); - - // dispatch new item position in local state when item position is changed by another user - socket.on( - 'item position data from server', - (itemPositionData: object) => { - // console.log( - // 'item position data received from server', - // itemPositionData - // ); - store.dispatch(changePosition(itemPositionData)); - } - ); - - // dispatch addComponent to local state when new component is created by another user - socket.on('new component data from server', (newComponent: object) => { - store.dispatch(addComponent(newComponent)); - }); - - // dispatch addElement to local state when new element is created by another user - socket.on('new element data from server', (newElement: object) => { - store.dispatch(addElement(newElement)); - }); - - // dispatch addState to local state when component state has been changed by another user - socket.on( - 'new component state data from server', - (componentState: object) => { - store.dispatch(addState(componentState)); - } - ); - - // dispatch deleteState to local state when component state has been deleted by another user - socket.on( - 'delete component state data from server', - (componentStateDelete: object) => { - store.dispatch(deleteState(componentStateDelete)); - } - ); - - // dispatch addPassedInProps to local state when p.I.P have been added by another user - socket.on( - 'new PassedInProps data from server', - (passedInProps: object) => { - store.dispatch(addPassedInProps(passedInProps)); - } - ); - - // dispatch deletePassedInProps to local state when p.I.P have been deleted by another user - socket.on( - 'PassedInProps delete data from server', - (passedInProps: object) => { - store.dispatch(deletePassedInProps(passedInProps)); - } - ); - - // dispatch addContext to local state when context has been changed by another user - socket.on('new context from server', (context: AddContextPayload) => { - store.dispatch(addContext(context)); - }); - - // dispatch addContextValues to local state when context values are added by another user - socket.on( - 'new context value from server', - (contextVal: AddContextValuesPayload) => { - store.dispatch(addContextValues(contextVal)); - } - ); - - // dispatch deleteContext to local state when context is deleted by another user - socket.on( - 'delete context data from server', - (context: DeleteContextPayload) => { - store.dispatch(deleteContext(context)); - } - ); - - // dispatch addComponentToContext to local state when context is assigned to component by another user - socket.on('assign context data from server', (data) => { - store.dispatch( - addComponentToContext({ - context: data.context, - component: data.component - }) - ); - store.dispatch( - deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) - ); - }); - } - }; - - const createNewCollabRoom = () => { - if (userList.length !== 0) { - dispatch(setUserList([])); - } - - initSocketConnection(roomCode, roomPassword, 'CREATE'); - }; - - const addNewUserToCollabRoom = () => { - dispatch(setRoomCode(roomCode)); - dispatch(setPassword(roomPassword)); - dispatch(setUserJoinCollabRoom(true)); - }; - - const joinExistingCollabRoom = async () => { - if (userList.length !== 0) { - dispatch(setUserList([])); - } - - initSocketConnection(roomCode, joinedPasswordAttempt, 'JOIN'); - }; - - const leaveRoom = () => { - let socket = getSocket(); - - if (socket) { - socket.disconnect(); - } - - dispatch(setRoomCode('')); - dispatch(setUserName('')); - dispatch(setUserList([])); - dispatch(setUserJoinCollabRoom(false)); //false: join room UI appear - dispatch(resetState('')); - dispatch(setPassword('')); - dispatch(setEmptyMessages([])); - dispatch(setUseMic(false)); - dispatch(setUseWebcam(false)); - }; - - const checkInputField = (...inputs) => { - let userName: string = inputs[0].trim(); - let roomCode: string = inputs[1].trim(); - let password: string = inputs[2].trim(); - return ( - userName.length === 0 || roomCode.length === 0 || password.length === 0 - ); - }; - - const handleKeyDown = (e) => { - if (e.key === 'Enter' && e.target.id === 'filled-hidden-label-small') { - e.preventDefault(); - createNewCollabRoom(); - } - }; - - const userColors = [ - '#0671e3', - '#2fd64d', - '#f0c000', - '#fb4c64', - '#be5be8', - '#fe9c06', - '#f6352b', - '#1667d1', - '#1667d1', - '#50ed6a' - ]; - - return ( -
    - - - Live Room: {roomCode} - - {userJoinCollabRoom ? ( - <> - - Nickname: {userName} - - - Users: {userList.length} - - - - {userList.map((user, index) => ( - - - - ))} - - - - - ) : ( - <> - dispatch(setUserName(e.target.value))} - /> - {isJoinCallabRoom ? ( - dispatch(setRoomCode(e.target.value))} - className="enterRoomInput" - onKeyDown={handleKeyDown} - helperText={ - isRoomAvailable === false ? `Room doesn't exist` : '' - } - /> - ) : ( - dispatch(setRoomCode(e.target.value))} - className="enterRoomInput" - onKeyDown={handleKeyDown} - helperText={isCollabRoomTaken ? 'Room name already taken' : ''} - /> - )} - {isJoinCallabRoom ? ( - setJoinedPasswordAttempt(e.target.value)} - /> - ) : ( - dispatch(setPassword(e.target.value))} - /> - )} - - - setIsJoinCollabRoom(!isJoinCallabRoom)} - sx={{ - color: 'grey', - '&:hover': { - textDecoration: 'underline' - } - }} - > - {isJoinCallabRoom ? 'Start a new room' : 'Join a room'} - - - )} - -
    - ); -}; - -export default RoomsContainer; +import { useState } from 'react'; +import { Stack, Typography } from '@mui/material'; +import { useDispatch, useSelector } from 'react-redux'; +import Box from '@mui/material/Box'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import Button from '@mui/material/Button'; +import { RootState } from '../../redux/store'; +import TextField from '@mui/material/TextField'; +import { BottomPanelObj } from '../../interfaces/Interfaces'; +import { + allCooperativeState, + addChild, + changeFocus, + deleteChild, + changePosition, + resetState, + updateStateUsed, + updateUseContext, + updateCss, + updateAttributes, + updateEvents, + addComponent, + addElement, + addState, + deleteState, + addPassedInProps, + deletePassedInProps, + deleteElement, + resetAllState, + updateStylesheet +} from '../../redux/reducers/slice/appStateSlice'; +import { + addContext, + deleteContext, + addContextValues +} from '../../redux/reducers/slice/contextReducer'; +import { + setRoomCode, + setUserName, + setUserJoinCollabRoom, + setUserList, + setMeetingId, + setMessages, + setEmptyMessages, + setPassword, + setUseMic, + setUseWebcam +} from '../../redux/reducers/slice/roomSlice'; +import { codePreviewCooperative } from '../../redux/reducers/slice/codePreviewSlice'; +import { cooperativeStyle } from '../../redux/reducers/slice/styleSlice'; +import store from '../../redux/store'; +import { initializeSocket, getSocket } from '../../helperFunctions/socket'; +import { + AddContextPayload, + AddContextValuesPayload, + DeleteContextPayload, + addComponentToContext +} from '../../../src/redux/reducers/slice/contextReducer'; + +const RoomsContainer = () => { + const [isJoinCallabRoom, setIsJoinCollabRoom] = useState(false); + const [joinedPasswordAttempt, setJoinedPasswordAttempt] = useState(''); + const [isPasswordAttemptIncorrect, setIsPasswordAttemptIncorrect] = + useState(true); + const [isCollabRoomTaken, setIsCollabRoomTaken] = useState(false); + const [isRoomAvailable, setIsRoomAvailable] = useState(true); + + const dispatch = useDispatch(); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const userName = useSelector((store: RootState) => store.roomSlice.userName); + const userList = useSelector((store: RootState) => store.roomSlice.userList); + const roomPassword = useSelector( + (store: RootState) => store.roomSlice.password + ); + + + const userJoinCollabRoom = useSelector( + (store: RootState) => store.roomSlice.userJoinCollabRoom + ); + + const messages = useSelector((store: RootState) => store.roomSlice.messages); + + const initSocketConnection = ( + roomCode: string, + roomPassword: string, + method: string + ) => { + // helper function to create socket connection + initializeSocket(); + // assign socket to result of helper function to return socket created + const socket = getSocket(); + // if socket was created correctly and exists + if (socket) { + //run everytime when a client connects to server + socket.on('connect', () => { + socket.emit( + 'creating a room', + userName, + roomCode, + roomPassword, + method + ); + }); + + socket.on('wrong password', () => { + setIsPasswordAttemptIncorrect(false); + }); + + socket.on('correct password', () => { + setIsPasswordAttemptIncorrect(true); + addNewUserToCollabRoom(); + }); + + socket.on('user created a new room', () => { + addNewUserToCollabRoom(); + }); + + socket.on('room is already taken', () => { + setIsCollabRoomTaken(true); + }); + + socket.on('room does not exist', () => { + setIsRoomAvailable(false); + + }); + //If you are the host: send current state to server when a new user joins + socket.on('requesting state from host', (callback) => { + const newState = store.getState(); //pull the current state + callback(newState); //send it to backend server + }); + + //If you are the new user: receive the state from the host + socket.on('server emitting state from host', (state, callback) => { + //dispatching new state to change user current state + // console.log('state received by new join:', state); + store.dispatch(allCooperativeState(state.appState)); + store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); + store.dispatch(cooperativeStyle(state.styleSlice)); + callback({ status: 'confirmed' }); + }); + + // update user list when there's a change: new join or leave the room + socket.on('update room information', (messageData) => { + //console.log('user list received from server'); + if (messageData.userList) dispatch(setUserList(messageData.userList)); + if (messageData.meetingId) + dispatch(setMeetingId(messageData.meetingId)); + }); + + socket.on('new chat message', (messageData) => { + if ( + messages.length === 0 || + JSON.stringify(messageData) !== JSON.stringify(messages[-1]) + ) { + dispatch(setMessages(messageData)); + } + }); + + // dispatch add child to local state when element has been added by another user + socket.on('child data from server', (childData: object) => { + // console.log('child data received by users', childData); + store.dispatch(addChild(childData)); + }); + + // dispatch changeFocus to local state when another user has changed focus by selecting element on canvas + socket.on('focus data from server', (focusData: object) => { + // console.log('focus data received from server', focusData); + store.dispatch(changeFocus(focusData)); + }); + + // dispatch deleteChild to local state when another user has deleted an element + socket.on('delete data from server', (deleteData: object) => { + // console.log('delete data received from server', deleteData); + store.dispatch(deleteChild(deleteData)); + }); + + // dispatch delete element to local state when another user has deleted an element + socket.on( + 'delete element data from server', + (deleteElementData: object) => { + // console.log('delete element data received from server', deleteElementData); + store.dispatch(deleteElement(deleteElementData)); + } + ); + + // dispatch clear canvas action to local state when the host of the room has clear canvas + socket.on('clear canvas from server', () => { + store.dispatch(resetAllState()); + }); + + // dispatch all updates to local state when another user has saved from Bottom Panel + socket.on('update data from server', (updateData: BottomPanelObj) => { + // console.log('update data received from server', updateData); + store.dispatch( + updateStateUsed({ + stateUsedObj: updateData.stateUsedObj, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateUseContext({ + useContextObj: updateData.useContextObj, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateCss({ + style: updateData.style, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateAttributes({ + attributes: updateData.attributes, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateEvents({ + events: updateData.events, + contextParam: updateData.contextParam + }) + ); + }); + + // dispatch update style in local state when CSS panel is updated on their side + socket.on('update css data from server', (cssData: object) => { + // console.log('CSS data received from server', cssData); + store.dispatch(updateStylesheet(cssData)); + }); + + // dispatch new item position in local state when item position is changed by another user + socket.on( + 'item position data from server', + (itemPositionData: object) => { + // console.log( + // 'item position data received from server', + // itemPositionData + // ); + store.dispatch(changePosition(itemPositionData)); + } + ); + + // dispatch addComponent to local state when new component is created by another user + socket.on('new component data from server', (newComponent: object) => { + store.dispatch(addComponent(newComponent)); + }); + + // dispatch addElement to local state when new element is created by another user + socket.on('new element data from server', (newElement: object) => { + store.dispatch(addElement(newElement)); + }); + + // dispatch addState to local state when component state has been changed by another user + socket.on( + 'new component state data from server', + (componentState: object) => { + store.dispatch(addState(componentState)); + } + ); + + // dispatch deleteState to local state when component state has been deleted by another user + socket.on( + 'delete component state data from server', + (componentStateDelete: object) => { + store.dispatch(deleteState(componentStateDelete)); + } + ); + + // dispatch addPassedInProps to local state when p.I.P have been added by another user + socket.on( + 'new PassedInProps data from server', + (passedInProps: object) => { + store.dispatch(addPassedInProps(passedInProps)); + } + ); + + // dispatch deletePassedInProps to local state when p.I.P have been deleted by another user + socket.on( + 'PassedInProps delete data from server', + (passedInProps: object) => { + store.dispatch(deletePassedInProps(passedInProps)); + } + ); + + // dispatch addContext to local state when context has been changed by another user + socket.on('new context from server', (context: AddContextPayload) => { + store.dispatch(addContext(context)); + }); + + // dispatch addContextValues to local state when context values are added by another user + socket.on( + 'new context value from server', + (contextVal: AddContextValuesPayload) => { + store.dispatch(addContextValues(contextVal)); + } + ); + + // dispatch deleteContext to local state when context is deleted by another user + socket.on( + 'delete context data from server', + (context: DeleteContextPayload) => { + store.dispatch(deleteContext(context)); + } + ); + + // dispatch addComponentToContext to local state when context is assigned to component by another user + socket.on('assign context data from server', (data) => { + store.dispatch( + addComponentToContext({ + context: data.context, + component: data.component + }) + ); + store.dispatch( + deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) + ); + }); + } + }; + + const createNewCollabRoom = () => { + if (userList.length !== 0) { + dispatch(setUserList([])); + } + + initSocketConnection(roomCode, roomPassword, 'CREATE'); + }; + + const addNewUserToCollabRoom = () => { + dispatch(setRoomCode(roomCode)); + dispatch(setPassword(roomPassword)); + dispatch(setUserJoinCollabRoom(true)); + }; + + const joinExistingCollabRoom = async () => { + if (userList.length !== 0) { + dispatch(setUserList([])); + } + + initSocketConnection(roomCode, joinedPasswordAttempt, 'JOIN'); + }; + + const leaveRoom = () => { + let socket = getSocket(); + + if (socket) { + socket.disconnect(); + } + + dispatch(setRoomCode('')); + dispatch(setUserName('')); + dispatch(setUserList([])); + dispatch(setUserJoinCollabRoom(false)); //false: join room UI appear + dispatch(resetState('')); + dispatch(setPassword('')); + dispatch(setEmptyMessages([])); + dispatch(setUseMic(false)); + dispatch(setUseWebcam(false)); + }; + + const checkInputField = (...inputs) => { + let userName: string = inputs[0].trim(); + let roomCode: string = inputs[1].trim(); + let password: string = inputs[2].trim(); + return ( + userName.length === 0 || roomCode.length === 0 || password.length === 0 + ); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Enter' && e.target.id === 'filled-hidden-label-small') { + e.preventDefault(); + createNewCollabRoom(); + } + }; + + const userColors = [ + '#0671e3', + '#2fd64d', + '#f0c000', + '#fb4c64', + '#be5be8', + '#fe9c06', + '#f6352b', + '#1667d1', + '#1667d1', + '#50ed6a' + ]; + + return ( +
    + + + Live Room: {roomCode} + + {userJoinCollabRoom ? ( + <> + + Nickname: {userName} + + + Users: {userList.length} + + + + {userList.map((user, index) => ( + + + + ))} + + + + + ) : ( + <> + dispatch(setUserName(e.target.value))} + /> + {isJoinCallabRoom ? ( + dispatch(setRoomCode(e.target.value))} + className="enterRoomInput" + onKeyDown={handleKeyDown} + helperText={ + isRoomAvailable === false ? `Room doesn't exist` : '' + } + /> + ) : ( + dispatch(setRoomCode(e.target.value))} + className="enterRoomInput" + onKeyDown={handleKeyDown} + helperText={isCollabRoomTaken ? 'Room name already taken' : ''} + /> + )} + {isJoinCallabRoom ? ( + setJoinedPasswordAttempt(e.target.value)} + /> + ) : ( + dispatch(setPassword(e.target.value))} + /> + )} + + + setIsJoinCollabRoom(!isJoinCallabRoom)} + sx={{ + color: 'grey', + '&:hover': { + textDecoration: 'underline' + } + }} + > + {isJoinCallabRoom ? 'Start a new room' : 'Join a room'} + + + )} + +
    + ); +}; + +export default RoomsContainer; diff --git a/app/src/components/left/Settings.tsx b/app/src/components/left/Settings.tsx index 77bb0fde8..99e915617 100644 --- a/app/src/components/left/Settings.tsx +++ b/app/src/components/left/Settings.tsx @@ -1,39 +1,39 @@ -import React from 'react'; -import { RootState } from '../../redux/store'; -import makeStyles from '@mui/styles/makeStyles'; -import { useSelector } from 'react-redux'; - -const ProfilePage = () => { - const classes = useStyles(); - - return ( -
    -
    -
    -

    Settings

    -
    -
    -
    - ); -}; - -const useStyles = makeStyles({ - panelWrapper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - flexGrow: 1, - overflow: 'auto' - }, - panelWrapperList: { - minHeight: '120px' - }, - lightThemeFontColor: { - color: '#fff' - }, - darkThemeFontColor: { - color: '#fff' - } -}); - -export default ProfilePage; +import React from 'react'; +import { RootState } from '../../redux/store'; +import makeStyles from '@mui/styles/makeStyles'; +import { useSelector } from 'react-redux'; + +const ProfilePage = () => { + const classes = useStyles(); + + return ( +
    +
    +
    +

    Settings

    +
    +
    +
    + ); +}; + +const useStyles = makeStyles({ + panelWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flexGrow: 1, + overflow: 'auto' + }, + panelWrapperList: { + minHeight: '120px' + }, + lightThemeFontColor: { + color: '#fff' + }, + darkThemeFontColor: { + color: '#fff' + } +}); + +export default ProfilePage; diff --git a/app/src/components/left/Sidebar.tsx b/app/src/components/left/Sidebar.tsx index 77c87a444..d29c2528d 100644 --- a/app/src/components/left/Sidebar.tsx +++ b/app/src/components/left/Sidebar.tsx @@ -1,131 +1,131 @@ -import { Tab, Tabs } from '@mui/material'; - -import { AddBox, Groups2, AccountBox, Settings } from '@mui/icons-material'; -import { IoMdCube } from 'react-icons/io'; -import React from 'react'; - -interface SidebarProps { - activeTab: number | null; - setActiveTab: (value: number | null) => void; - toggleVisibility: (state: boolean) => void; -} - -let oldValue = 0; - -const Sidebar: React.FC = ({ - activeTab, - setActiveTab, - toggleVisibility -}) => { - const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setActiveTab(newValue); - toggleVisibility(true); - oldValue = newValue; - }; - - //the following allows users to click on the left panel to expand and collapse. - // We decided to freeze so we've commented this and line 41 out - - // const handleTabClick = (event: React.MouseEvent, oldValue: number) => { - // if (activeTab === oldValue) { - // setActiveTab(null); - // // toggleVisibility(false); - // } - // }; - - return ( - handleTabClick(e, oldValue)} - TabIndicatorProps={{ - style: { - backgroundColor: '#4A4A4A' - } - }} - sx={{ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'top', - gap: '50px', - width: '70px', - background: '#1e2024', - marginRight: '2px', - height: '100vh' - }} - > - - } - value={0} - label="Canvas" - /> - } - value={1} - label="Create" - /> - } - value={2} - label="Collab" - /> - } - value={3} - label="Profile" - /> - } - value={4} - label="Settings" - /> - - ); -}; - -export default Sidebar; +import { Tab, Tabs } from '@mui/material'; + +import { AddBox, Groups2, AccountBox, Settings } from '@mui/icons-material'; +import { IoMdCube } from 'react-icons/io'; +import React from 'react'; + +interface SidebarProps { + activeTab: number | null; + setActiveTab: (value: number | null) => void; + toggleVisibility: (state: boolean) => void; +} + +let oldValue = 0; + +const Sidebar: React.FC = ({ + activeTab, + setActiveTab, + toggleVisibility +}) => { + const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setActiveTab(newValue); + toggleVisibility(true); + oldValue = newValue; + }; + + //the following allows users to click on the left panel to expand and collapse. + // We decided to freeze so we've commented this and line 41 out + + // const handleTabClick = (event: React.MouseEvent, oldValue: number) => { + // if (activeTab === oldValue) { + // setActiveTab(null); + // // toggleVisibility(false); + // } + // }; + + return ( + handleTabClick(e, oldValue)} + TabIndicatorProps={{ + style: { + backgroundColor: '#4A4A4A' + } + }} + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'top', + gap: '50px', + width: '70px', + background: '#1e2024', + marginRight: '2px', + height: '100vh' + }} + > + + } + value={0} + label="Canvas" + /> + } + value={1} + label="Create" + /> + } + value={2} + label="Collab" + /> + } + value={3} + label="Profile" + /> + } + value={4} + label="Settings" + /> + + ); +}; + +export default Sidebar; diff --git a/app/src/components/login/FBPassWord.tsx b/app/src/components/login/FBPassWord.tsx index 717b855d2..ff79fb0ea 100644 --- a/app/src/components/login/FBPassWord.tsx +++ b/app/src/components/login/FBPassWord.tsx @@ -1,206 +1,206 @@ -import React, { useState, MouseEvent } from 'react'; -import { LoginInt } from '../../interfaces/Interfaces'; -import { - Link as RouteLink, - withRouter, - RouteComponentProps -} from 'react-router-dom'; -import { newUserIsCreated } from '../../helperFunctions/auth'; -import Avatar from '@mui/material/Avatar'; -import Button from '@mui/material/Button'; -import CssBaseline from '@mui/material/CssBaseline'; -import TextField from '@mui/material/TextField'; -import Grid from '@mui/material/Grid'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import makeStyles from '@mui/styles/makeStyles'; -import Container from '@mui/material/Container'; - -function Copyright() { - return ( - - {'Copyright © ReacType '} - {new Date().getFullYear()} - {'.'} - - ); -} - -const useStyles = makeStyles((theme) => ({ - paper: { - marginTop: theme.spacing(8), - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: '#3EC1AC' - }, - form: { - width: '100%', // Fix IE 11 issue. - marginTop: theme.spacing(3) - }, - submit: { - margin: theme.spacing(3, 0, 2) - }, - root: { - '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderColor: '#3EC1AC' - } - } -})); - -const SignUp: React.FC = (props) => { - const classes = useStyles(); - const [password, setPassword] = useState(''); - const [passwordVerify, setPasswordVerify] = useState(''); - - const [invalidPasswordMsg, setInvalidPasswordMsg] = useState(''); - const [invalidVerifyPasswordMsg, setInvalidVerifyPasswordMsg] = useState(''); - - const [invalidPassword, setInvalidPassword] = useState(false); - const [invalidVerifyPassword, setInvalidVerifyPassword] = useState(false); - - const handleChange = (e: React.ChangeEvent) => { - let inputVal = e.target.value; - switch (e.target.name) { - case 'password': - setPassword(inputVal); - break; - case 'passwordVerify': - setPasswordVerify(inputVal); - break; - } - }; - - const handleSignUp = (e: React.MouseEvent) => { - e.preventDefault(); - const email = props.location.state.email; - // Reset Error Validation - setInvalidPasswordMsg(''); - setInvalidVerifyPasswordMsg(''); - setInvalidPassword(false); - setInvalidVerifyPassword(false); - - if (password === '') { - setInvalidPassword(true); - setInvalidPasswordMsg('No Password Entered'); - return; - } else if (password.length < 8) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 8 Characters'); - return; - } else if ( - !/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i.test( - password - ) - ) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 1 Letter, Number, and Special Character'); - return; - } else if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); - return; - } else { - setInvalidPassword(false); - } - - if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); - return; - } else { - setInvalidVerifyPassword(false); - } - - // get username and email from FB - newUserIsCreated(email, email, password).then((userCreated) => { - if (userCreated === 'Success') { - props.history.push('/'); - } else { - } - }); - }; - - return ( - - -
    - - - - - Please enter in your new password - -
    - - - - - - - - - - + - - - - Already have an account? Sign In - - - -
    -
    - - - -
    - ); -}; - -export default withRouter(SignUp); +import React, { useState, MouseEvent } from 'react'; +import { LoginInt } from '../../interfaces/Interfaces'; +import { + Link as RouteLink, + withRouter, + RouteComponentProps +} from 'react-router-dom'; +import { newUserIsCreated } from '../../helperFunctions/auth'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import CssBaseline from '@mui/material/CssBaseline'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import makeStyles from '@mui/styles/makeStyles'; +import Container from '@mui/material/Container'; + +function Copyright() { + return ( + + {'Copyright © ReacType '} + {new Date().getFullYear()} + {'.'} + + ); +} + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(8), + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: '#3EC1AC' + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(3) + }, + submit: { + margin: theme.spacing(3, 0, 2) + }, + root: { + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: '#3EC1AC' + } + } +})); + +const SignUp: React.FC = (props) => { + const classes = useStyles(); + const [password, setPassword] = useState(''); + const [passwordVerify, setPasswordVerify] = useState(''); + + const [invalidPasswordMsg, setInvalidPasswordMsg] = useState(''); + const [invalidVerifyPasswordMsg, setInvalidVerifyPasswordMsg] = useState(''); + + const [invalidPassword, setInvalidPassword] = useState(false); + const [invalidVerifyPassword, setInvalidVerifyPassword] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + let inputVal = e.target.value; + switch (e.target.name) { + case 'password': + setPassword(inputVal); + break; + case 'passwordVerify': + setPasswordVerify(inputVal); + break; + } + }; + + const handleSignUp = (e: React.MouseEvent) => { + e.preventDefault(); + const email = props.location.state.email; + // Reset Error Validation + setInvalidPasswordMsg(''); + setInvalidVerifyPasswordMsg(''); + setInvalidPassword(false); + setInvalidVerifyPassword(false); + + if (password === '') { + setInvalidPassword(true); + setInvalidPasswordMsg('No Password Entered'); + return; + } else if (password.length < 8) { + setInvalidPassword(true); + setInvalidPasswordMsg('Minimum 8 Characters'); + return; + } else if ( + !/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i.test( + password + ) + ) { + setInvalidPassword(true); + setInvalidPasswordMsg('Minimum 1 Letter, Number, and Special Character'); + return; + } else if (password !== passwordVerify) { + setInvalidPassword(true); + setInvalidVerifyPassword(true); + setInvalidPasswordMsg('Verification Failed'); + setInvalidVerifyPasswordMsg('Verification Failed'); + setPasswordVerify(''); + return; + } else { + setInvalidPassword(false); + } + + if (password !== passwordVerify) { + setInvalidPassword(true); + setInvalidVerifyPassword(true); + setInvalidPasswordMsg('Verification Failed'); + setInvalidVerifyPasswordMsg('Verification Failed'); + setPasswordVerify(''); + return; + } else { + setInvalidVerifyPassword(false); + } + + // get username and email from FB + newUserIsCreated(email, email, password).then((userCreated) => { + if (userCreated === 'Success') { + props.history.push('/'); + } else { + } + }); + }; + + return ( + + +
    + + + + + Please enter in your new password + +
    + + + + + + + + + + + + + + + Already have an account? Sign In + + + +
    +
    + + + +
    + ); +}; + +export default withRouter(SignUp); diff --git a/app/src/components/login/SignIn.tsx b/app/src/components/login/SignIn.tsx index b68766d28..f32d26428 100644 --- a/app/src/components/login/SignIn.tsx +++ b/app/src/components/login/SignIn.tsx @@ -1,392 +1,392 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { RouteComponentProps, Link as RouteLink } from 'react-router-dom'; -import { SigninDark } from '../../../../app/src/public/styles/theme'; -import { - StyledEngineProvider, - Theme, - ThemeProvider -} from '@mui/material/styles'; -import { useDispatch } from 'react-redux'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import { LoginInt } from '../../interfaces/Interfaces'; -import serverConfig from '../../serverConfig.js'; -import makeStyles from '@mui/styles/makeStyles'; -import { sessionIsCreated } from '../../helperFunctions/auth'; -import { - Divider, - Box, - Avatar, - Button, - Container, - CssBaseline, - Grid, - TextField, - Typography -} from '@mui/material'; - -const { API_BASE_URL } = serverConfig; - -declare module '@mui/styles/defaultTheme' { - interface DefaultTheme extends Theme {} -} - -function Copyright() { - return ( - - {'Copyright © ReacType '} - {new Date().getFullYear()} - {'.'} - - ); -} - -const useStyles = makeStyles((theme) => ({ - paper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }, - avatar: { - backgroundColor: 'white' - }, - form: { - width: '100%' - }, - submit: { - cursor: 'pointer' - }, - root: { - '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderColor: 'white' - } - } -})); - -const SignIn: React.FC = (props) => { - const classes = useStyles(); - const dispatch = useDispatch(); - - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [invalidUserMsg, setInvalidUserMsg] = useState(''); - const [invalidPassMsg, setInvalidPassMsg] = useState(''); - const [invalidUser, setInvalidUser] = useState(false); - const [invalidPass, setInvalidPass] = useState(false); - - useEffect(() => { - const githubCookie = setInterval(() => { - window.api?.setCookie(); - window.api?.getCookie((cookie) => { - if (cookie[0]) { - window.localStorage.setItem('ssid', cookie[0].value); - clearInterval(githubCookie); - props.history.push('/'); - } else if (window.localStorage.getItem('ssid')) { - clearInterval(githubCookie); - } - }); - }, 2000); - }, []); - - const handleChange = (e: React.ChangeEvent) => { - let inputVal = e.target.value; - - switch (e.target.name) { - case 'username': - setUsername(inputVal); - break; - - case 'password': - setPassword(inputVal); - break; - } - }; - - const handleLogin = (e: React.MouseEvent) => { - e.preventDefault(); - setInvalidUser(false); - setInvalidUserMsg(''); - setInvalidPass(false); - setInvalidPassMsg(''); - sessionIsCreated(username, password, false).then((loginStatus) => { - if (loginStatus === 'Success') { - props.history.push('/'); - } else { - switch (loginStatus) { - case 'No Username Input': - setInvalidUser(true); - setInvalidUserMsg(loginStatus); - break; - - case 'No Password Input': - setInvalidPass(true); - setInvalidPassMsg(loginStatus); - break; - - case 'Invalid Username': - setInvalidUser(true); - setInvalidUserMsg(loginStatus); - break; - - case 'Incorrect Password': - setInvalidPass(true); - setInvalidPassMsg(loginStatus); - break; - } - } - }); - }; - - const keyBindSignIn = useCallback((e) => { - if (e.key === 'Enter') { - e.preventDefault(); - document.getElementById('SignIn').click(); - } - }, []); - - useEffect(() => { - document.addEventListener('keydown', keyBindSignIn); - return () => { - document.removeEventListener('keydown', keyBindSignIn); - }; - }, []); - - const handleLoginGuest = ( - e: React.MouseEvent - ) => { - e.preventDefault(); - window.localStorage.setItem('ssid', 'guest'); - props.history.push('/'); - }; - - const handleGithubLogin = ( - e: React.MouseEvent - ) => { - e.preventDefault(); - window.location.assign(`${API_BASE_URL}/auth/github`); - }; - - const classBtn = - 'MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-fullWidth'; - - return ( - - - - -
    - - - - - Log in - - - - - - - - OR - - - handleGithubLogin(e)} - sx={{ - marginBottom: '1rem', - textTransform: 'none', - fontSize: '1rem', - color: 'white', - '$:hover': { - cursor: 'pointer', - color: 'black', - textDecoration: 'underline' - } - }} - > - - - - Sign In With Github - - { - e.preventDefault(); - window.location.assign(`${API_BASE_URL}/auth/google`); - }} - sx={{ - marginBottom: '1rem', - textTransform: 'none', - fontSize: '1rem', - color: 'white', - '$:hover': { - cursor: 'pointer', - color: 'black' - } - }} - > - - - - Sign in With Google - - handleLoginGuest(e)} - sx={{ - marginBottom: '1rem', - textTransform: 'none', - fontSize: '1rem', - color: 'white', - '$:hover': { - cursor: 'pointer', - color: 'black' - } - }} - > - - - - - Continue as Guest - - - Forgot password? - - - - - Don't have an account? - Sign Up - - -
    - - - -
    -
    -
    - ); -}; - -export default SignIn; +import React, { useCallback, useEffect, useState } from 'react'; +import { RouteComponentProps, Link as RouteLink } from 'react-router-dom'; +import { SigninDark } from '../../../../app/src/public/styles/theme'; +import { + StyledEngineProvider, + Theme, + ThemeProvider +} from '@mui/material/styles'; +import { useDispatch } from 'react-redux'; +import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import { LoginInt } from '../../interfaces/Interfaces'; +import serverConfig from '../../serverConfig.js'; +import makeStyles from '@mui/styles/makeStyles'; +import { sessionIsCreated } from '../../helperFunctions/auth'; +import { + Divider, + Box, + Avatar, + Button, + Container, + CssBaseline, + Grid, + TextField, + Typography +} from '@mui/material'; + +const { API_BASE_URL } = serverConfig; + +declare module '@mui/styles/defaultTheme' { + interface DefaultTheme extends Theme {} +} + +function Copyright() { + return ( + + {'Copyright © ReacType '} + {new Date().getFullYear()} + {'.'} + + ); +} + +const useStyles = makeStyles((theme) => ({ + paper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + avatar: { + backgroundColor: 'white' + }, + form: { + width: '100%' + }, + submit: { + cursor: 'pointer' + }, + root: { + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: 'white' + } + } +})); + +const SignIn: React.FC = (props) => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [invalidUserMsg, setInvalidUserMsg] = useState(''); + const [invalidPassMsg, setInvalidPassMsg] = useState(''); + const [invalidUser, setInvalidUser] = useState(false); + const [invalidPass, setInvalidPass] = useState(false); + + useEffect(() => { + const githubCookie = setInterval(() => { + window.api?.setCookie(); + window.api?.getCookie((cookie) => { + if (cookie[0]) { + window.localStorage.setItem('ssid', cookie[0].value); + clearInterval(githubCookie); + props.history.push('/'); + } else if (window.localStorage.getItem('ssid')) { + clearInterval(githubCookie); + } + }); + }, 2000); + }, []); + + const handleChange = (e: React.ChangeEvent) => { + let inputVal = e.target.value; + + switch (e.target.name) { + case 'username': + setUsername(inputVal); + break; + + case 'password': + setPassword(inputVal); + break; + } + }; + + const handleLogin = (e: React.MouseEvent) => { + e.preventDefault(); + setInvalidUser(false); + setInvalidUserMsg(''); + setInvalidPass(false); + setInvalidPassMsg(''); + sessionIsCreated(username, password, false).then((loginStatus) => { + if (loginStatus === 'Success') { + props.history.push('/'); + } else { + switch (loginStatus) { + case 'No Username Input': + setInvalidUser(true); + setInvalidUserMsg(loginStatus); + break; + + case 'No Password Input': + setInvalidPass(true); + setInvalidPassMsg(loginStatus); + break; + + case 'Invalid Username': + setInvalidUser(true); + setInvalidUserMsg(loginStatus); + break; + + case 'Incorrect Password': + setInvalidPass(true); + setInvalidPassMsg(loginStatus); + break; + } + } + }); + }; + + const keyBindSignIn = useCallback((e) => { + if (e.key === 'Enter') { + e.preventDefault(); + document.getElementById('SignIn').click(); + } + }, []); + + useEffect(() => { + document.addEventListener('keydown', keyBindSignIn); + return () => { + document.removeEventListener('keydown', keyBindSignIn); + }; + }, []); + + const handleLoginGuest = ( + e: React.MouseEvent + ) => { + e.preventDefault(); + window.localStorage.setItem('ssid', 'guest'); + props.history.push('/'); + }; + + const handleGithubLogin = ( + e: React.MouseEvent + ) => { + e.preventDefault(); + window.location.assign(`${API_BASE_URL}/auth/github`); + }; + + const classBtn = + 'MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-fullWidth'; + + return ( + + + + +
    + + + + + Log in + + + + + + + + OR + + + handleGithubLogin(e)} + sx={{ + marginBottom: '1rem', + textTransform: 'none', + fontSize: '1rem', + color: 'white', + '$:hover': { + cursor: 'pointer', + color: 'black', + textDecoration: 'underline' + } + }} + > + + + + Sign In With Github + + { + e.preventDefault(); + window.location.assign(`${API_BASE_URL}/auth/google`); + }} + sx={{ + marginBottom: '1rem', + textTransform: 'none', + fontSize: '1rem', + color: 'white', + '$:hover': { + cursor: 'pointer', + color: 'black' + } + }} + > + + + + Sign in With Google + + handleLoginGuest(e)} + sx={{ + marginBottom: '1rem', + textTransform: 'none', + fontSize: '1rem', + color: 'white', + '$:hover': { + cursor: 'pointer', + color: 'black' + } + }} + > + + + + + Continue as Guest + + + Forgot password? + + + + + Don't have an account? + Sign Up + + +
    + + + +
    +
    +
    + ); +}; + +export default SignIn; diff --git a/app/src/components/login/SignUp.tsx b/app/src/components/login/SignUp.tsx index 59305c90d..5def1d5b2 100644 --- a/app/src/components/login/SignUp.tsx +++ b/app/src/components/login/SignUp.tsx @@ -1,370 +1,370 @@ -import React, { useState } from 'react'; -import { - RouteComponentProps, - Link as RouteLink, - withRouter -} from 'react-router-dom'; -import { - SigninDark, - SigninLight -} from '../../../../app/src/public/styles/theme'; -import { - StyledEngineProvider, - Theme, - ThemeProvider -} from '@mui/material/styles'; -import { useDispatch, useSelector } from 'react-redux'; -import { - Box, - Avatar, - Button, - Container, - CssBaseline, - Grid, - TextField, - Typography -} from '@mui/material'; - -import AssignmentIcon from '@mui/icons-material/Assignment'; -import { LoginInt } from '../../interfaces/Interfaces'; -import { RootState } from '../../redux/store'; - -import makeStyles from '@mui/styles/makeStyles'; -import { newUserIsCreated } from '../../helperFunctions/auth'; - -declare module '@mui/styles/defaultTheme' { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface DefaultTheme extends Theme {} -} - -function Copyright() { - return ( - - {'Copyright © ReacType '} - {new Date().getFullYear()} - - ); -} - -const useStyles = makeStyles((theme) => ({ - paper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }, - avatar: { - backgroundColor: 'white' - }, - form: { - width: '100%' // Fix IE 11 issue. - }, - submit: {}, - root: { - '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderColor: 'white' - } - } -})); - -const SignUp: React.FC = (props) => { - const classes = useStyles(); - const dispatch = useDispatch(); - const [email, setEmail] = useState(''); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [passwordVerify, setPasswordVerify] = useState(''); - const [invalidEmailMsg, setInvalidEmailMsg] = useState(''); - const [invalidUsernameMsg, setInvalidUsernameMsg] = useState(''); - const [invalidPasswordMsg, setInvalidPasswordMsg] = useState(''); - const [invalidVerifyPasswordMsg, setInvalidVerifyPasswordMsg] = useState(''); - const [invalidEmail, setInvalidEmail] = useState(false); - const [invalidUsername, setInvalidUsername] = useState(false); - const [invalidPassword, setInvalidPassword] = useState(false); - const [invalidVerifyPassword, setInvalidVerifyPassword] = useState(false); - - const handleChange = (e: React.ChangeEvent) => { - let inputVal = e.target.value; - switch (e.target.name) { - case 'email': - setEmail(inputVal); - break; - case 'username': - setUsername(inputVal); - break; - case 'password': - setPassword(inputVal); - break; - case 'passwordVerify': - setPasswordVerify(inputVal); - break; - } - }; - - const handleSignUp = (e: React.MouseEvent) => { - e.preventDefault(); - - // Reset Error Validation - setInvalidEmailMsg(''); - setInvalidUsernameMsg(''); - setInvalidPasswordMsg(''); - setInvalidVerifyPasswordMsg(''); - setInvalidEmail(false); - setInvalidUsername(false); - setInvalidPassword(false); - setInvalidVerifyPassword(false); - - if (email === '') { - setInvalidEmail(true); - setInvalidEmailMsg('No Email Entered'); - return; - } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) { - setInvalidEmail(true); - setInvalidEmailMsg('Invalid Email Format'); - return; - } else { - setInvalidEmail(false); - } - - if (username === '') { - setInvalidUsername(true); - setInvalidUsernameMsg('No Username Entered'); - return; - } else if (!/^[\w\s-]{4,15}$/i.test(username)) { - setInvalidUsername(true); - setInvalidUsernameMsg('Must Be 4 - 15 Characters Long'); - return; - } else if (!/^[\w-]+$/i.test(username)) { - setInvalidUsername(true); - setInvalidUsernameMsg('Cannot Contain Spaces or Special Characters'); - return; - } else { - setInvalidUsername(false); - } - - if (password === '') { - setInvalidPassword(true); - setInvalidPasswordMsg('No Password Entered'); - return; - } else if (password.length < 8) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 8 Characters'); - return; - } else if ( - !/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i.test( - password - ) - ) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 1 Letter, Number, and Special Character'); - return; - } else if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); - return; - } else { - setInvalidPassword(false); - } - - if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); - return; - } else { - setInvalidVerifyPassword(false); - } - - newUserIsCreated(username, email, password).then((userCreated) => { - if (userCreated === 'Success') { - props.history.push('/'); - } else { - switch (userCreated) { - case 'Email Taken': - setInvalidEmail(true); - setInvalidEmailMsg('Email Taken'); - break; - case 'Username Taken': - setInvalidUsername(true); - setInvalidUsernameMsg('Username Taken'); - break; - } - } - }); - }; - - return ( - - - - -
    - - - - - Sign up - -
    - - - - - - - - - - - - - - - - - By signing up, you agree to our - - {' '} - Terms , Privacy Policy - {' '} - and - {' '} - Cookies Policy - {' '} - - - - - - - Already have an account? - Log in{' '} - - -
    -
    - - - -
    -
    -
    - ); -}; - -export default withRouter(SignUp); +import React, { useState } from 'react'; +import { + RouteComponentProps, + Link as RouteLink, + withRouter +} from 'react-router-dom'; +import { + SigninDark, + SigninLight +} from '../../../../app/src/public/styles/theme'; +import { + StyledEngineProvider, + Theme, + ThemeProvider +} from '@mui/material/styles'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Box, + Avatar, + Button, + Container, + CssBaseline, + Grid, + TextField, + Typography +} from '@mui/material'; + +import AssignmentIcon from '@mui/icons-material/Assignment'; +import { LoginInt } from '../../interfaces/Interfaces'; +import { RootState } from '../../redux/store'; + +import makeStyles from '@mui/styles/makeStyles'; +import { newUserIsCreated } from '../../helperFunctions/auth'; + +declare module '@mui/styles/defaultTheme' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface DefaultTheme extends Theme {} +} + +function Copyright() { + return ( + + {'Copyright © ReacType '} + {new Date().getFullYear()} + + ); +} + +const useStyles = makeStyles((theme) => ({ + paper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + avatar: { + backgroundColor: 'white' + }, + form: { + width: '100%' // Fix IE 11 issue. + }, + submit: {}, + root: { + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: 'white' + } + } +})); + +const SignUp: React.FC = (props) => { + const classes = useStyles(); + const dispatch = useDispatch(); + const [email, setEmail] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [passwordVerify, setPasswordVerify] = useState(''); + const [invalidEmailMsg, setInvalidEmailMsg] = useState(''); + const [invalidUsernameMsg, setInvalidUsernameMsg] = useState(''); + const [invalidPasswordMsg, setInvalidPasswordMsg] = useState(''); + const [invalidVerifyPasswordMsg, setInvalidVerifyPasswordMsg] = useState(''); + const [invalidEmail, setInvalidEmail] = useState(false); + const [invalidUsername, setInvalidUsername] = useState(false); + const [invalidPassword, setInvalidPassword] = useState(false); + const [invalidVerifyPassword, setInvalidVerifyPassword] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + let inputVal = e.target.value; + switch (e.target.name) { + case 'email': + setEmail(inputVal); + break; + case 'username': + setUsername(inputVal); + break; + case 'password': + setPassword(inputVal); + break; + case 'passwordVerify': + setPasswordVerify(inputVal); + break; + } + }; + + const handleSignUp = (e: React.MouseEvent) => { + e.preventDefault(); + + // Reset Error Validation + setInvalidEmailMsg(''); + setInvalidUsernameMsg(''); + setInvalidPasswordMsg(''); + setInvalidVerifyPasswordMsg(''); + setInvalidEmail(false); + setInvalidUsername(false); + setInvalidPassword(false); + setInvalidVerifyPassword(false); + + if (email === '') { + setInvalidEmail(true); + setInvalidEmailMsg('No Email Entered'); + return; + } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) { + setInvalidEmail(true); + setInvalidEmailMsg('Invalid Email Format'); + return; + } else { + setInvalidEmail(false); + } + + if (username === '') { + setInvalidUsername(true); + setInvalidUsernameMsg('No Username Entered'); + return; + } else if (!/^[\w\s-]{4,15}$/i.test(username)) { + setInvalidUsername(true); + setInvalidUsernameMsg('Must Be 4 - 15 Characters Long'); + return; + } else if (!/^[\w-]+$/i.test(username)) { + setInvalidUsername(true); + setInvalidUsernameMsg('Cannot Contain Spaces or Special Characters'); + return; + } else { + setInvalidUsername(false); + } + + if (password === '') { + setInvalidPassword(true); + setInvalidPasswordMsg('No Password Entered'); + return; + } else if (password.length < 8) { + setInvalidPassword(true); + setInvalidPasswordMsg('Minimum 8 Characters'); + return; + } else if ( + !/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i.test( + password + ) + ) { + setInvalidPassword(true); + setInvalidPasswordMsg('Minimum 1 Letter, Number, and Special Character'); + return; + } else if (password !== passwordVerify) { + setInvalidPassword(true); + setInvalidVerifyPassword(true); + setInvalidPasswordMsg('Verification Failed'); + setInvalidVerifyPasswordMsg('Verification Failed'); + setPasswordVerify(''); + return; + } else { + setInvalidPassword(false); + } + + if (password !== passwordVerify) { + setInvalidPassword(true); + setInvalidVerifyPassword(true); + setInvalidPasswordMsg('Verification Failed'); + setInvalidVerifyPasswordMsg('Verification Failed'); + setPasswordVerify(''); + return; + } else { + setInvalidVerifyPassword(false); + } + + newUserIsCreated(username, email, password).then((userCreated) => { + if (userCreated === 'Success') { + props.history.push('/'); + } else { + switch (userCreated) { + case 'Email Taken': + setInvalidEmail(true); + setInvalidEmailMsg('Email Taken'); + break; + case 'Username Taken': + setInvalidUsername(true); + setInvalidUsernameMsg('Username Taken'); + break; + } + } + }); + }; + + return ( + + + + +
    + + + + + Sign up + +
    + + + + + + + + + + + + + + + + + By signing up, you agree to our + + {' '} + Terms , Privacy Policy + {' '} + and + {' '} + Cookies Policy + {' '} + + + + + + + Already have an account? + Log in{' '} + + +
    +
    + + + +
    +
    +
    + ); +}; + +export default withRouter(SignUp); diff --git a/app/src/components/main/AddLink.tsx b/app/src/components/main/AddLink.tsx index e11979667..fd2b50660 100644 --- a/app/src/components/main/AddLink.tsx +++ b/app/src/components/main/AddLink.tsx @@ -1,104 +1,104 @@ -import React, { useState } from 'react'; -import FormControl from '@mui/material/FormControl'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; -import { InputLabel } from '@mui/material'; -import { useDispatch, useSelector } from 'react-redux'; -import { updateAttributes } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; - -function AddLink({ id, onClickHandler, linkDisplayed }) { - const { state, contextParam, isThemeLight } = useSelector( - (store: RootState) => ({ - state: store.appState, - contextParam: store.contextSlice, - isThemeLight: store.styleSlice - }) - ); - const dispatch = useDispatch(); - //this function allows the link to be functional when it's nested - function deepIterate(arr) { - const output = []; - for (let i = 0; i < arr.length; i++) { - if (arr[i].typeId === 1000) continue; - output.push(arr[i]); - if (arr[i].children.length) { - output.push(...deepIterate(arr[i].children)); - } - } - return output; - } - - const handlePageSelect = (event) => { - const currComponent = state.components.find( - (element) => element.id === state.canvasFocus.componentId - ); - deepIterate(currComponent.children).some((element) => { - if (element.childId === id) { - const state = JSON.parse(JSON.stringify(element)); - state.childId = id; - state.attributes.compLink = event.target.value; - dispatch( - updateAttributes({ attributes: state, contextParam: contextParam }) - ); - return true; - } - }); - }; - - const pagesItems = state.components.filter((comp) => - state.rootComponents.includes(comp.id) - ); - const dropDown = [ - - ].concat( - pagesItems.map((comp) => ( - - {comp.name} - - )) - ); - - return ( -
    - - - Pages - - - -
    - ); -} - -export default AddLink; +import React, { useState } from 'react'; +import FormControl from '@mui/material/FormControl'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import { InputLabel } from '@mui/material'; +import { useDispatch, useSelector } from 'react-redux'; +import { updateAttributes } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; + +function AddLink({ id, onClickHandler, linkDisplayed }) { + const { state, contextParam, isThemeLight } = useSelector( + (store: RootState) => ({ + state: store.appState, + contextParam: store.contextSlice, + isThemeLight: store.styleSlice + }) + ); + const dispatch = useDispatch(); + //this function allows the link to be functional when it's nested + function deepIterate(arr) { + const output = []; + for (let i = 0; i < arr.length; i++) { + if (arr[i].typeId === 1000) continue; + output.push(arr[i]); + if (arr[i].children.length) { + output.push(...deepIterate(arr[i].children)); + } + } + return output; + } + + const handlePageSelect = (event) => { + const currComponent = state.components.find( + (element) => element.id === state.canvasFocus.componentId + ); + deepIterate(currComponent.children).some((element) => { + if (element.childId === id) { + const state = JSON.parse(JSON.stringify(element)); + state.childId = id; + state.attributes.compLink = event.target.value; + dispatch( + updateAttributes({ attributes: state, contextParam: contextParam }) + ); + return true; + } + }); + }; + + const pagesItems = state.components.filter((comp) => + state.rootComponents.includes(comp.id) + ); + const dropDown = [ + + ].concat( + pagesItems.map((comp) => ( + + {comp.name} + + )) + ); + + return ( +
    + + + Pages + + + +
    + ); +} + +export default AddLink; diff --git a/app/src/components/main/AddRoute.tsx b/app/src/components/main/AddRoute.tsx index b061cb8e2..5807b1b1b 100644 --- a/app/src/components/main/AddRoute.tsx +++ b/app/src/components/main/AddRoute.tsx @@ -1,42 +1,42 @@ -import { AddRoutes } from '../../interfaces/Interfaces'; -import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { addChild } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; -import { emitEvent } from '../../helperFunctions/socket'; - -function AddRoute({ id }: AddRoutes): JSX.Element { - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const dispatch = useDispatch(); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const handleClick = (id: number): void => { - dispatch( - addChild({ - type: 'HTML Element', - typeId: -1, - childId: id, // this is the id of the parent to attach it to - contextParam: contextParam - }) - ); - if (roomCode) { - emitEvent('addChildAction', roomCode, { - type: 'HTML Element', - typeId: -1, - childId: id, - contextParam: contextParam - }); - - console.log('emit addChildAction event is triggered in AddRoute!'); - } - }; - - return ( -
    - -
    - ); -} - -export default AddRoute; +import { AddRoutes } from '../../interfaces/Interfaces'; +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { addChild } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; + +function AddRoute({ id }: AddRoutes): JSX.Element { + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const dispatch = useDispatch(); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const handleClick = (id: number): void => { + dispatch( + addChild({ + type: 'HTML Element', + typeId: -1, + childId: id, // this is the id of the parent to attach it to + contextParam: contextParam + }) + ); + if (roomCode) { + emitEvent('addChildAction', roomCode, { + type: 'HTML Element', + typeId: -1, + childId: id, + contextParam: contextParam + }); + + console.log('emit addChildAction event is triggered in AddRoute!'); + } + }; + + return ( +
    + +
    + ); +} + +export default AddRoute; diff --git a/app/src/components/main/Arrow.tsx b/app/src/components/main/Arrow.tsx index 91c3a8910..c43a4cf34 100644 --- a/app/src/components/main/Arrow.tsx +++ b/app/src/components/main/Arrow.tsx @@ -1,87 +1,87 @@ -import { Arrow } from '../../interfaces/Interfaces' -const arrow: Arrow = { - renderArrow: (id) => { - if(id != null) { - let canvasEle = document.getElementById(`canv${id}`) - let renderEle = document.getElementById(`rend${id}`) - if( canvasEle === null || renderEle === null) { - return; - } else { - let canvasElePosition = canvasEle.getBoundingClientRect(); - let renderElePosition = renderEle.getBoundingClientRect(); - const canvasEleX = canvasElePosition.left + canvasElePosition.width; - const canvasEleY = canvasElePosition.top + (canvasElePosition.height / 2); - const renderEleX = renderElePosition.left; - const renderEleY = renderElePosition.top; - arrow.lineDraw(canvasEleX, canvasEleY, renderEleX, renderEleY); - } - } - }, - - createLineElement: (x, y, length, angle) => { - let styles = 'border: 1px solid black;' - + 'width: ' + length + 'px;' - + 'height: 0px;' - + 'z-index: 9999999999;' - + '-moz-transform: rotate(' + angle + 'rad);' - + '-webkit-transform: rotate(' + angle + 'rad);' - + '-o-transform: rotate(' + angle + 'rad);' - + '-ms-transform: rotate(' + angle + 'rad);' - + 'position: absolute;' - + 'top: ' + y + 'px;' - + 'left: ' + x + 'px;'; - let line = document.createElement("div"); - line.setAttribute("class", "line"); - line.setAttribute('style', styles); - return line; - }, - - createHeadElement: (x, y, length, angle) => { - let styles = 'width: 13px;' - + 'height: 13px;' - + 'border: solid 2px;' - + 'border-radius: 50%;' - + 'z-index: 9999999999;' - + 'background-color: #00FFFF;' - + 'border-color: #bbb;' - + 'rotate: ' + angle + 'rad;' - + 'position: absolute;' - + 'top: ' + -6.5 + 'px;' - + 'left: ' + 0 + (length - 12) + 'px;'; - let head = document.createElement("div"); - head.setAttribute("class", "head"); - head.setAttribute('style', styles); - return head; - }, - - lineDraw: (x1, y1, x2, y2) => { - let a = x1 - x2, - b = y1 - y2, - c = Math.sqrt(a * a + b * b); - let sx = (x1 + x2) / 2, - sy = (y1 + y2) / 2; - let x = sx - c / 2, - y = sy; - let alpha = Math.PI - Math.atan2(-b, a); - let line = arrow.createLineElement(x, y, c, alpha); - let head = arrow.createHeadElement(x, y, c, alpha); - arrow.deleteLines(); - document.getElementsByClassName("main")[0].append(line); - document.getElementsByClassName("line")[0].append(head); - }, - - deleteLines: () => { - let lineArray = document.getElementsByClassName("line"); - let lineArrayIterable = Array.from(lineArray); - let headArray = document.getElementsByClassName("head"); - let headArrayIterable = Array.from(headArray); - for(const line of lineArrayIterable) { - line.remove(); - } - for(const head of headArrayIterable) { - head.remove(); - } - } -} - -export default arrow; +import { Arrow } from '../../interfaces/Interfaces' +const arrow: Arrow = { + renderArrow: (id) => { + if(id != null) { + let canvasEle = document.getElementById(`canv${id}`) + let renderEle = document.getElementById(`rend${id}`) + if( canvasEle === null || renderEle === null) { + return; + } else { + let canvasElePosition = canvasEle.getBoundingClientRect(); + let renderElePosition = renderEle.getBoundingClientRect(); + const canvasEleX = canvasElePosition.left + canvasElePosition.width; + const canvasEleY = canvasElePosition.top + (canvasElePosition.height / 2); + const renderEleX = renderElePosition.left; + const renderEleY = renderElePosition.top; + arrow.lineDraw(canvasEleX, canvasEleY, renderEleX, renderEleY); + } + } + }, + + createLineElement: (x, y, length, angle) => { + let styles = 'border: 1px solid black;' + + 'width: ' + length + 'px;' + + 'height: 0px;' + + 'z-index: 9999999999;' + + '-moz-transform: rotate(' + angle + 'rad);' + + '-webkit-transform: rotate(' + angle + 'rad);' + + '-o-transform: rotate(' + angle + 'rad);' + + '-ms-transform: rotate(' + angle + 'rad);' + + 'position: absolute;' + + 'top: ' + y + 'px;' + + 'left: ' + x + 'px;'; + let line = document.createElement("div"); + line.setAttribute("class", "line"); + line.setAttribute('style', styles); + return line; + }, + + createHeadElement: (x, y, length, angle) => { + let styles = 'width: 13px;' + + 'height: 13px;' + + 'border: solid 2px;' + + 'border-radius: 50%;' + + 'z-index: 9999999999;' + + 'background-color: #00FFFF;' + + 'border-color: #bbb;' + + 'rotate: ' + angle + 'rad;' + + 'position: absolute;' + + 'top: ' + -6.5 + 'px;' + + 'left: ' + 0 + (length - 12) + 'px;'; + let head = document.createElement("div"); + head.setAttribute("class", "head"); + head.setAttribute('style', styles); + return head; + }, + + lineDraw: (x1, y1, x2, y2) => { + let a = x1 - x2, + b = y1 - y2, + c = Math.sqrt(a * a + b * b); + let sx = (x1 + x2) / 2, + sy = (y1 + y2) / 2; + let x = sx - c / 2, + y = sy; + let alpha = Math.PI - Math.atan2(-b, a); + let line = arrow.createLineElement(x, y, c, alpha); + let head = arrow.createHeadElement(x, y, c, alpha); + arrow.deleteLines(); + document.getElementsByClassName("main")[0].append(line); + document.getElementsByClassName("line")[0].append(head); + }, + + deleteLines: () => { + let lineArray = document.getElementsByClassName("line"); + let lineArrayIterable = Array.from(lineArray); + let headArray = document.getElementsByClassName("head"); + let headArrayIterable = Array.from(headArray); + for(const line of lineArrayIterable) { + line.remove(); + } + for(const head of headArrayIterable) { + head.remove(); + } + } +} + +export default arrow; diff --git a/app/src/components/main/Canvas.tsx b/app/src/components/main/Canvas.tsx index c491dfd3f..1610cd084 100644 --- a/app/src/components/main/Canvas.tsx +++ b/app/src/components/main/Canvas.tsx @@ -1,389 +1,389 @@ -import { Component, DragItem } from '../../interfaces/Interfaces'; -import { DropTargetMonitor, useDrop } from 'react-dnd'; -import React, { useEffect, useState, forwardRef } from 'react'; -import { - addChild, - changeFocus, - snapShotAction -} from '../../redux/reducers/slice/appStateSlice'; -import { useDispatch, useSelector } from 'react-redux'; -import { debounce, throttle } from 'lodash'; - -import Arrow from './Arrow'; -import { ItemTypes } from '../../constants/ItemTypes'; -import { RootState } from '../../redux/store'; -import { combineStyles } from '../../helperFunctions/combineStyles'; -import renderChildren from '../../helperFunctions/renderChildren'; -import { emitEvent, getSocket } from '../../helperFunctions/socket'; -import { FaMousePointer } from 'react-icons/fa'; -import { display } from 'html2canvas/dist/types/css/property-descriptors/display'; -import { ZoomIn, ZoomOut } from '@mui/icons-material'; -import { Button } from '@mui/material'; - -interface CanvasProps { - zoom: number; -} - -const Canvas = forwardRef(({ zoom }, ref) => { - const state = useSelector((store: RootState) => store.appState); - const contextParam = useSelector((store: RootState) => store.contextSlice); - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const userName = useSelector((store: RootState) => store.roomSlice.userName); - const userList = useSelector((store: RootState) => store.roomSlice.userList); - - //remote cursor data - const [remoteCursors, setRemoteCursors] = useState([]); - - // Toggle switch for live cursor tracking - const [toggleSwitch, setToggleSwitch] = useState(true); - - // Toggle button text for live cursor tracking button - on/off - const [toggleText, setToggleText] = useState('off'); - const toggleButton = () => { - setToggleText(toggleText === 'on' ? 'off' : 'on'); - }; - - // Prevents lagging and provides smoother user experience got live cursor tracking (milliseconds can be adjusted but 500ms is most optimal) - const debounceSetPosition = debounce((newX, newY) => { - //emit socket event every 300ms when cursor moves - if (userList.length > 1) - emitEvent('cursorData', roomCode, { x: newX, y: newY, userName }); - }, 500); - - const handleMouseMove = (e) => { - debounceSetPosition(e.clientX, e.clientY); - }; - - const handleCursorDataFromServer = (remoteData) => { - setRemoteCursors((prevState) => { - //check if received cursor data is from an existing user in the room - const cursorIdx = prevState.findIndex( - (cursor) => cursor.remoteUserName === remoteData.userName - ); - //existing user - if (cursorIdx >= 0) { - //check if cursor position has changed - if ( - prevState[cursorIdx].x !== remoteData.x || - prevState[cursorIdx].y !== remoteData.y - ) { - //update existing user's cursor position - const updatedCursors = [...prevState]; - updatedCursors[cursorIdx] = { - ...prevState[cursorIdx], - x: remoteData.x, - y: remoteData.y - }; - return updatedCursors; - } else { - //return previous state if no change - return prevState; - } - } else { - //new user: add new user's cursor - return [ - ...prevState, - { - x: remoteData.x, - y: remoteData.y, - remoteUserName: remoteData.userName, - isVisible: true - } - ]; - } - }); - }; - - // Removes the mouse cursor of the user that leaves the collaboration room. - const handleCursorDeleteFromServer = () => { - setRemoteCursors((prevRemoteCursors) => - // filter cursors to include only those in the userList - prevRemoteCursors.filter((cursor) => - userList.includes(cursor.remoteUserName) - ) - ); - }; - - // Function that will turn on/off socket depending on toggle Switch. - const handleToggleSwitch = () => { - setToggleSwitch(!toggleSwitch); - //checks the state before it's updated so need to check the opposite condition - if (toggleSwitch) { - socket.off('remote cursor data from server'); - //make remote cursor invisible - setRemoteCursors((prevState) => { - const newState = prevState.map((cursor) => ({ - ...cursor, - isVisible: false - })); - return newState; - }); - } else { - socket.on('remote cursor data from server', (remoteData) => - handleCursorDataFromServer(remoteData) - ); - //make remote cursor visible - setRemoteCursors((prevState) => - prevState.map((cursor) => ({ - ...cursor, - isVisible: true - })) - ); - } - }; - - //Function to handle the multiple click events of the toggle button. - const multipleClicks = () => { - handleToggleSwitch(); - toggleButton(); - }; - - const socket = getSocket(); - //wrap the socket event listener in useEffect with dependency array as [socket], so the the effect will run only when: 1. After the initial rendering of the component 2. Every time the socket instance changes(connect, disconnect) - useEffect(() => { - if (socket) { - socket.on('remote cursor data from server', (remoteData) => - handleCursorDataFromServer(remoteData) - ); - } - - return () => { - if (socket) socket.off('remote cursor data from server'); - }; - }, [socket]); - - useEffect(() => { - handleCursorDeleteFromServer(); - }, [userList]); - - // find the current component based on the canvasFocus component ID in the state - const currentComponent: Component = state.components.find( - (elem: Component) => elem.id === state.canvasFocus.componentId - ); - - Arrow.deleteLines(); - - const dispatch = useDispatch(); - // changes focus of the canvas to a new component / child - const changeFocusFunction = ( - componentId?: number, - childId?: number | null - ) => { - dispatch(changeFocus({ componentId, childId })); - //if room exists, send focus dispatch to all users - if (roomCode) { - emitEvent('changeFocusAction', roomCode, { - componentId: componentId, - childId: childId - }); - } - }; - - // onClickHandler is responsible for changing the focused component and child component - function onClickHandler(event: React.MouseEvent) { - event.stopPropagation(); - changeFocusFunction(state.canvasFocus.componentId, null); - } - - // stores a snapshot of state into the past array for UNDO. snapShotFunc is also invoked for nestable elements in DirectChildHTMLNestable.tsx - const snapShotFunc = () => { - // make a deep clone of state - const deepCopiedState = JSON.parse(JSON.stringify(state)); - const focusIndex = state.canvasFocus.componentId - 1; - dispatch( - snapShotAction({ - focusIndex: focusIndex, - deepCopiedState: deepCopiedState - }) - ); - }; - - // This hook will allow the user to drag items from the left panel on to the canvas - const [{ isOver }, drop] = useDrop({ - accept: ItemTypes.INSTANCE, - drop: (item: DragItem, monitor: DropTargetMonitor) => { - const didDrop = monitor.didDrop(); - // takes a snapshot of state to be used in UNDO and REDO cases - snapShotFunc(); - // returns false for direct drop target - if (didDrop) { - return; - } - // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component - if (item.newInstance && item.instanceType !== 'Component') { - dispatch( - //update state - addChild({ - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }) - ); - - //emit the socket event - if (roomCode) { - emitEvent('addChildAction', roomCode, { - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }); - } - } else if (item.newInstance && item.instanceType === 'Component') { - let hasDiffParent = false; - const components = state.components; - let newChildName = ''; - // loop over components array - for (let i = 0; i < components.length; i++) { - const comp = components[i]; - //loop over each componenets child - for (let j = 0; j < comp.children.length; j++) { - const child = comp.children[j]; - if (child.name === 'separator') continue; - // check if the item.instanceTypeId matches and child ID - if (item.instanceTypeId === child.typeId) { - // check if the name of the parent matches the canvas focus name - // comp is the parent component - // currentComponent is the canvas.focus component - if (comp.name === currentComponent.name) { - i = components.length; - break; - } else { - // if false - // setCopiedComp(child); - hasDiffParent = true; - newChildName = child.name; - i = components.length; - break; - } - } - } - } - // if (!hasDiffParent) { - dispatch( - addChild({ - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }) - ); - if (roomCode) { - emitEvent('addChildAction', roomCode, { - type: item.instanceType, - typeId: item.instanceTypeId, - childId: null, - contextParam: contextParam - }); - } - } - }, - collect: (monitor) => ({ - isOver: !!monitor.isOver() - }) - }); - - // Styling for Canvas - const defaultCanvasStyle: React.CSSProperties = { - width: '100%', - minHeight: '100%', - aspectRatio: 'auto 774 / 1200', - boxSizing: 'border-box', - transform: `scale(${zoom})`, - transformOrigin: 'top center' - }; - - // Combine the default styles of the canvas with the custom styles set by the user for that component - // The renderChildren function renders all direct children of a given component - // Direct children are draggable/clickable - - const canvasStyle: React.CSSProperties = combineStyles( - defaultCanvasStyle, - currentComponent.style - ); - - // Array of colors that color code users as they join the room (In a set order) - const userColors = [ - '#0671e3', - '#2fd64d', - '#f0c000', - '#fb4c64', - '#be5be8', - '#fe9c06', - '#f6352b', - '#1667d1', - '#1667d1', - '#50ed6a' - ]; - - const buttonStyle: React.CSSProperties = { - textAlign: 'center', - color: '#ffffff', - backgroundColor: '#151515', - zIndex: 0, - border: '2px solid #0671e3', - margin: '8px 0 0 8px' - }; - - return ( -
    - {renderChildren(currentComponent.children)} - {remoteCursors.map( - (cursor, idx) => - cursor.isVisible && ( -
    - {} - {cursor.remoteUserName} -
    - ) - )} - -
    - ); -}); - -export default Canvas; +import { Component, DragItem } from '../../interfaces/Interfaces'; +import { DropTargetMonitor, useDrop } from 'react-dnd'; +import React, { useEffect, useState, forwardRef } from 'react'; +import { + addChild, + changeFocus, + snapShotAction +} from '../../redux/reducers/slice/appStateSlice'; +import { useDispatch, useSelector } from 'react-redux'; +import { debounce, throttle } from 'lodash'; + +import Arrow from './Arrow'; +import { ItemTypes } from '../../constants/ItemTypes'; +import { RootState } from '../../redux/store'; +import { combineStyles } from '../../helperFunctions/combineStyles'; +import renderChildren from '../../helperFunctions/renderChildren'; +import { emitEvent, getSocket } from '../../helperFunctions/socket'; +import { FaMousePointer } from 'react-icons/fa'; +import { display } from 'html2canvas/dist/types/css/property-descriptors/display'; +import { ZoomIn, ZoomOut } from '@mui/icons-material'; +import { Button } from '@mui/material'; + +interface CanvasProps { + zoom: number; +} + +const Canvas = forwardRef(({ zoom }, ref) => { + const state = useSelector((store: RootState) => store.appState); + const contextParam = useSelector((store: RootState) => store.contextSlice); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const userName = useSelector((store: RootState) => store.roomSlice.userName); + const userList = useSelector((store: RootState) => store.roomSlice.userList); + + //remote cursor data + const [remoteCursors, setRemoteCursors] = useState([]); + + // Toggle switch for live cursor tracking + const [toggleSwitch, setToggleSwitch] = useState(true); + + // Toggle button text for live cursor tracking button - on/off + const [toggleText, setToggleText] = useState('off'); + const toggleButton = () => { + setToggleText(toggleText === 'on' ? 'off' : 'on'); + }; + + // Prevents lagging and provides smoother user experience got live cursor tracking (milliseconds can be adjusted but 500ms is most optimal) + const debounceSetPosition = debounce((newX, newY) => { + //emit socket event every 300ms when cursor moves + if (userList.length > 1) + emitEvent('cursorData', roomCode, { x: newX, y: newY, userName }); + }, 500); + + const handleMouseMove = (e) => { + debounceSetPosition(e.clientX, e.clientY); + }; + + const handleCursorDataFromServer = (remoteData) => { + setRemoteCursors((prevState) => { + //check if received cursor data is from an existing user in the room + const cursorIdx = prevState.findIndex( + (cursor) => cursor.remoteUserName === remoteData.userName + ); + //existing user + if (cursorIdx >= 0) { + //check if cursor position has changed + if ( + prevState[cursorIdx].x !== remoteData.x || + prevState[cursorIdx].y !== remoteData.y + ) { + //update existing user's cursor position + const updatedCursors = [...prevState]; + updatedCursors[cursorIdx] = { + ...prevState[cursorIdx], + x: remoteData.x, + y: remoteData.y + }; + return updatedCursors; + } else { + //return previous state if no change + return prevState; + } + } else { + //new user: add new user's cursor + return [ + ...prevState, + { + x: remoteData.x, + y: remoteData.y, + remoteUserName: remoteData.userName, + isVisible: true + } + ]; + } + }); + }; + + // Removes the mouse cursor of the user that leaves the collaboration room. + const handleCursorDeleteFromServer = () => { + setRemoteCursors((prevRemoteCursors) => + // filter cursors to include only those in the userList + prevRemoteCursors.filter((cursor) => + userList.includes(cursor.remoteUserName) + ) + ); + }; + + // Function that will turn on/off socket depending on toggle Switch. + const handleToggleSwitch = () => { + setToggleSwitch(!toggleSwitch); + //checks the state before it's updated so need to check the opposite condition + if (toggleSwitch) { + socket.off('remote cursor data from server'); + //make remote cursor invisible + setRemoteCursors((prevState) => { + const newState = prevState.map((cursor) => ({ + ...cursor, + isVisible: false + })); + return newState; + }); + } else { + socket.on('remote cursor data from server', (remoteData) => + handleCursorDataFromServer(remoteData) + ); + //make remote cursor visible + setRemoteCursors((prevState) => + prevState.map((cursor) => ({ + ...cursor, + isVisible: true + })) + ); + } + }; + + //Function to handle the multiple click events of the toggle button. + const multipleClicks = () => { + handleToggleSwitch(); + toggleButton(); + }; + + const socket = getSocket(); + //wrap the socket event listener in useEffect with dependency array as [socket], so the the effect will run only when: 1. After the initial rendering of the component 2. Every time the socket instance changes(connect, disconnect) + useEffect(() => { + if (socket) { + socket.on('remote cursor data from server', (remoteData) => + handleCursorDataFromServer(remoteData) + ); + } + + return () => { + if (socket) socket.off('remote cursor data from server'); + }; + }, [socket]); + + useEffect(() => { + handleCursorDeleteFromServer(); + }, [userList]); + + // find the current component based on the canvasFocus component ID in the state + const currentComponent: Component = state.components.find( + (elem: Component) => elem.id === state.canvasFocus.componentId + ); + + Arrow.deleteLines(); + + const dispatch = useDispatch(); + // changes focus of the canvas to a new component / child + const changeFocusFunction = ( + componentId?: number, + childId?: number | null + ) => { + dispatch(changeFocus({ componentId, childId })); + //if room exists, send focus dispatch to all users + if (roomCode) { + emitEvent('changeFocusAction', roomCode, { + componentId: componentId, + childId: childId + }); + } + }; + + // onClickHandler is responsible for changing the focused component and child component + function onClickHandler(event: React.MouseEvent) { + event.stopPropagation(); + changeFocusFunction(state.canvasFocus.componentId, null); + } + + // stores a snapshot of state into the past array for UNDO. snapShotFunc is also invoked for nestable elements in DirectChildHTMLNestable.tsx + const snapShotFunc = () => { + // make a deep clone of state + const deepCopiedState = JSON.parse(JSON.stringify(state)); + const focusIndex = state.canvasFocus.componentId - 1; + dispatch( + snapShotAction({ + focusIndex: focusIndex, + deepCopiedState: deepCopiedState + }) + ); + }; + + // This hook will allow the user to drag items from the left panel on to the canvas + const [{ isOver }, drop] = useDrop({ + accept: ItemTypes.INSTANCE, + drop: (item: DragItem, monitor: DropTargetMonitor) => { + const didDrop = monitor.didDrop(); + // takes a snapshot of state to be used in UNDO and REDO cases + snapShotFunc(); + // returns false for direct drop target + if (didDrop) { + return; + } + // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component + if (item.newInstance && item.instanceType !== 'Component') { + dispatch( + //update state + addChild({ + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }) + ); + + //emit the socket event + if (roomCode) { + emitEvent('addChildAction', roomCode, { + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }); + } + } else if (item.newInstance && item.instanceType === 'Component') { + let hasDiffParent = false; + const components = state.components; + let newChildName = ''; + // loop over components array + for (let i = 0; i < components.length; i++) { + const comp = components[i]; + //loop over each componenets child + for (let j = 0; j < comp.children.length; j++) { + const child = comp.children[j]; + if (child.name === 'separator') continue; + // check if the item.instanceTypeId matches and child ID + if (item.instanceTypeId === child.typeId) { + // check if the name of the parent matches the canvas focus name + // comp is the parent component + // currentComponent is the canvas.focus component + if (comp.name === currentComponent.name) { + i = components.length; + break; + } else { + // if false + // setCopiedComp(child); + hasDiffParent = true; + newChildName = child.name; + i = components.length; + break; + } + } + } + } + // if (!hasDiffParent) { + dispatch( + addChild({ + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }) + ); + if (roomCode) { + emitEvent('addChildAction', roomCode, { + type: item.instanceType, + typeId: item.instanceTypeId, + childId: null, + contextParam: contextParam + }); + } + } + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver() + }) + }); + + // Styling for Canvas + const defaultCanvasStyle: React.CSSProperties = { + width: '100%', + minHeight: '100%', + aspectRatio: 'auto 774 / 1200', + boxSizing: 'border-box', + transform: `scale(${zoom})`, + transformOrigin: 'top center' + }; + + // Combine the default styles of the canvas with the custom styles set by the user for that component + // The renderChildren function renders all direct children of a given component + // Direct children are draggable/clickable + + const canvasStyle: React.CSSProperties = combineStyles( + defaultCanvasStyle, + currentComponent.style + ); + + // Array of colors that color code users as they join the room (In a set order) + const userColors = [ + '#0671e3', + '#2fd64d', + '#f0c000', + '#fb4c64', + '#be5be8', + '#fe9c06', + '#f6352b', + '#1667d1', + '#1667d1', + '#50ed6a' + ]; + + const buttonStyle: React.CSSProperties = { + textAlign: 'center', + color: '#ffffff', + backgroundColor: '#151515', + zIndex: 0, + border: '2px solid #0671e3', + margin: '8px 0 0 8px' + }; + + return ( +
    + {renderChildren(currentComponent.children)} + {remoteCursors.map( + (cursor, idx) => + cursor.isVisible && ( +
    + {} + {cursor.remoteUserName} +
    + ) + )} + +
    + ); +}); + +export default Canvas; diff --git a/app/src/components/main/CanvasContainer.tsx b/app/src/components/main/CanvasContainer.tsx index ad059e899..45740f3cf 100644 --- a/app/src/components/main/CanvasContainer.tsx +++ b/app/src/components/main/CanvasContainer.tsx @@ -1,153 +1,153 @@ -import React, { useState, useEffect, useRef } from 'react'; -import Canvas from './Canvas'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../../redux/store'; -import CodePreview from '../bottom/CodePreview'; -import { toggleCodePreview } from '../../redux/reducers/slice/appStateSlice'; -import { Button } from '@mui/material'; -import { - DeveloperMode, - VerticalAlignBottom, - VerticalAlignTop, - ZoomIn, - ZoomOut -} from '@mui/icons-material'; - -interface CanvasContainerProps { - zoom: number; - theme: string; - setTheme: React.Dispatch>; -} - -// The CanvasContainer sets the boundaries on the width/height of the canvas -function CanvasContainer(props: CanvasContainerProps): JSX.Element { - const [theme, setTheme] = useState('solarized_light'); // theme for ACE editor, taken from BottomTabs - const state = useSelector((store: RootState) => store.appState); - const dispatch = useDispatch(); - - // onClickCodePreview swaps the rendered component from the canvas to the code preview editor - const onClickCodePreview = () => { - dispatch(toggleCodePreview()); - }; - - const canvasContainerStyle: React.CSSProperties = { - width: '100%', - height: '100%', - background: '#070808', - backgroundImage: 'radial-gradient(#1E2024 .71px, transparent 0)', - backgroundSize: '8px 8px', - backgroundPosition: '-19px -19px', - borderBottom: 'none', - overflow: 'auto' - }; - - //containerRef references the container that will ultimately have the scroll functionality - const containerRef = useRef(null); - - const container = document.getElementById('canvasContainer'); - const components = document.querySelector('.componentContainer'); - - const [zoom, setZoom] = useState(1); - - const zoomIn = () => { - setZoom(zoom + 0.1); - }; - - const zoomOut = () => { - setZoom(Math.max(zoom - 0.1, 0.1)); - }; - - //useEffect dependency is the length of the parent components. No changes in children will scroll to the bottom. Once elements surpass the view of the canvas, scroll to bottom, else, keep scroll bar to the top. - useEffect(() => { - if ( - container && - components && - state.components[0].children.length > 0 && - components.scrollHeight == components.clientHeight - ) { - container.scrollTop = 0; - } else if (container && components) { - container.scrollTop = container.scrollHeight; - } - }, [state.components[0].children.length, zoom]); - - const buttonStyle: React.CSSProperties = { - textAlign: 'center', - color: '#ffffff', - backgroundColor: '#2D313A', - zIndex: 0, - whiteSpace: 'nowrap', - textTransform: 'none', - padding: '10px', - borderRadius: '0' - } as const; - - const codePreviewStyle: React.CSSProperties = { - borderRadius: '10px' - } as const; - - const upArrowStyle: React.CSSProperties = { - borderRadius: '10px 0 0 10px' - } as const; - - const zoomOutStyle: React.CSSProperties = { - borderRadius: '0 10px 10px 0' - } as const; - - return ( -
    -
    - - {!state.codePreview && ( -
    - - - - -
    - )} -
    - {state.codePreview ? ( - - ) : ( - - )} -
    - ); -} - -export default CanvasContainer; +import React, { useState, useEffect, useRef } from 'react'; +import Canvas from './Canvas'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; +import CodePreview from '../bottom/CodePreview'; +import { toggleCodePreview } from '../../redux/reducers/slice/appStateSlice'; +import { Button } from '@mui/material'; +import { + DeveloperMode, + VerticalAlignBottom, + VerticalAlignTop, + ZoomIn, + ZoomOut +} from '@mui/icons-material'; + +interface CanvasContainerProps { + zoom: number; + theme: string; + setTheme: React.Dispatch>; +} + +// The CanvasContainer sets the boundaries on the width/height of the canvas +function CanvasContainer(props: CanvasContainerProps): JSX.Element { + const [theme, setTheme] = useState('solarized_light'); // theme for ACE editor, taken from BottomTabs + const state = useSelector((store: RootState) => store.appState); + const dispatch = useDispatch(); + + // onClickCodePreview swaps the rendered component from the canvas to the code preview editor + const onClickCodePreview = () => { + dispatch(toggleCodePreview()); + }; + + const canvasContainerStyle: React.CSSProperties = { + width: '100%', + height: '100%', + background: '#070808', + backgroundImage: 'radial-gradient(#1E2024 .71px, transparent 0)', + backgroundSize: '8px 8px', + backgroundPosition: '-19px -19px', + borderBottom: 'none', + overflow: 'auto' + }; + + //containerRef references the container that will ultimately have the scroll functionality + const containerRef = useRef(null); + + const container = document.getElementById('canvasContainer'); + const components = document.querySelector('.componentContainer'); + + const [zoom, setZoom] = useState(1); + + const zoomIn = () => { + setZoom(zoom + 0.1); + }; + + const zoomOut = () => { + setZoom(Math.max(zoom - 0.1, 0.1)); + }; + + //useEffect dependency is the length of the parent components. No changes in children will scroll to the bottom. Once elements surpass the view of the canvas, scroll to bottom, else, keep scroll bar to the top. + useEffect(() => { + if ( + container && + components && + state.components[0].children.length > 0 && + components.scrollHeight == components.clientHeight + ) { + container.scrollTop = 0; + } else if (container && components) { + container.scrollTop = container.scrollHeight; + } + }, [state.components[0].children.length, zoom]); + + const buttonStyle: React.CSSProperties = { + textAlign: 'center', + color: '#ffffff', + backgroundColor: '#2D313A', + zIndex: 0, + whiteSpace: 'nowrap', + textTransform: 'none', + padding: '10px', + borderRadius: '0' + } as const; + + const codePreviewStyle: React.CSSProperties = { + borderRadius: '10px' + } as const; + + const upArrowStyle: React.CSSProperties = { + borderRadius: '10px 0 0 10px' + } as const; + + const zoomOutStyle: React.CSSProperties = { + borderRadius: '0 10px 10px 0' + } as const; + + return ( +
    +
    + + {!state.codePreview && ( +
    + + + + +
    + )} +
    + {state.codePreview ? ( + + ) : ( + + )} +
    + ); +} + +export default CanvasContainer; diff --git a/app/src/components/main/DeleteButton.tsx b/app/src/components/main/DeleteButton.tsx index 0a7d22e24..5e3ae0e21 100644 --- a/app/src/components/main/DeleteButton.tsx +++ b/app/src/components/main/DeleteButton.tsx @@ -1,47 +1,47 @@ -import React from 'react'; -import { DeleteButtons } from '../../interfaces/Interfaces'; -import { useDispatch, useSelector } from 'react-redux'; -import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; -import { RootState } from '../../redux/store'; -import { emitEvent } from '../../helperFunctions/socket'; -import { Clear } from '@mui/icons-material'; - -function DeleteButton({ id, name, onClickHandler }: DeleteButtons) { - const contextParam = useSelector((store: RootState) => store.contextSlice); - - const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - - const dispatch = useDispatch(); - - const deleteHTMLtype = (id: number) => { - dispatch(deleteChild({ id: id, contextParam: contextParam })); - if (roomCode) { - emitEvent('deleteChildAction', roomCode, { - id, - contextParam - }); - - // console.log( - // 'emit deleteChildAction event is triggered in DeleteButton.tsx' - // ); - } - }; - - return ( -
    - -
    - ); -} - -export default DeleteButton; +import React from 'react'; +import { DeleteButtons } from '../../interfaces/Interfaces'; +import { useDispatch, useSelector } from 'react-redux'; +import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; +import { Clear } from '@mui/icons-material'; + +function DeleteButton({ id, name, onClickHandler }: DeleteButtons) { + const contextParam = useSelector((store: RootState) => store.contextSlice); + + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + + const dispatch = useDispatch(); + + const deleteHTMLtype = (id: number) => { + dispatch(deleteChild({ id: id, contextParam: contextParam })); + if (roomCode) { + emitEvent('deleteChildAction', roomCode, { + id, + contextParam + }); + + // console.log( + // 'emit deleteChildAction event is triggered in DeleteButton.tsx' + // ); + } + }; + + return ( +
    + +
    + ); +} + +export default DeleteButton; diff --git a/app/src/components/main/DemoRender.tsx b/app/src/components/main/DemoRender.tsx index 8022b7ffd..14764c970 100644 --- a/app/src/components/main/DemoRender.tsx +++ b/app/src/components/main/DemoRender.tsx @@ -1,239 +1,239 @@ -import { - Link, - Route, - BrowserRouter as Router, - Switch, - useHistory -} from 'react-router-dom'; -import React, { useEffect, useRef } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import Box from '@mui/material/Box'; -import { Component } from '../../interfaces/Interfaces'; -import ReactDOMServer from 'react-dom/server'; -import { RootState } from '../../redux/store'; -import { changeFocus } from '../../redux/reducers/slice/appStateSlice'; - -// DemoRender is the full sandbox demo of our user's custom built React components. DemoRender references the design specifications stored in state to construct -// real react components that utilize hot module reloading to depict the user's prototype application. -const DemoRender = (): JSX.Element => { - const state = useSelector((store: RootState) => store.appState); - const stylesheet = useSelector( - (store: RootState) => store.appState.stylesheet - ); - const backHome = useHistory(); - const dispatch = useDispatch(); - - // Create React ref to inject transpiled code in inframe - const iframe = useRef(); - const demoContainerStyle = { - width: '100%', - backgroundColor: '#FBFBFB', - borderBottom: 'none', - overflow: 'auto' - }; - - const html = ` - - - - -
    -
    - - - - `; - - window.onmessage = (event) => { - // If event.data or event.data.data is undefined, return early - if (!event.data || typeof event.data.data !== 'string') return; - - const component: string = event.data.data.split('/').at(-1); - - // If components aren't defined or component isn't a string, return early - if (!state.components || !component) return; - - const matchedComponent = state.components.find( - (el) => el.name.toLowerCase() === component.toLowerCase() - ); - - // If matchedComponent is undefined or doesn't have an id, return early - if (!matchedComponent || matchedComponent.id === undefined) return; - - dispatch(changeFocus({ componentId: matchedComponent.id, childId: null })); - }; - - // This function is the heart of DemoRender it will take the array of components stored in state and dynamically construct the desired React component for the live demo - // Material UI is utilized to incorporate the apporpriate tags with specific configuration designs as necessitated by HTML standards. - const componentBuilder = (array: any, key: number = 0) => { - const componentsToRender = []; - for (const element of array) { - if (element.name !== 'separator') { - const elementType = element.name; - const childId = element.childId; - const elementStyle = element.style; - const innerText = element.attributes.compText; - const classRender = element.attributes.cssClasses; - const activeLink = element.attributes.compLink; - let renderedChildren; - if ( - elementType !== 'input' && - elementType !== 'img' && - elementType !== 'Image' && - element.children.length > 0 - ) { - renderedChildren = componentBuilder(element.children); - } - if (elementType === 'input') - componentsToRender.push( - - ); - else if (elementType === 'img') - componentsToRender.push( - - ); - else if (elementType === 'Image') - componentsToRender.push( - - ); - else if (elementType === 'a' || elementType === 'Link') - componentsToRender.push( - - {innerText} - {renderedChildren} - - ); - else if (elementType === 'Switch') - componentsToRender.push({renderedChildren}); - else if (elementType === 'Route') - componentsToRender.push( - - {renderedChildren} - - ); - else - componentsToRender.push( - - {innerText} - {renderedChildren} - - ); - key += 1; - } - } - return componentsToRender; - }; - - //initializes our 'code' which will be whats actually in the iframe in the demo render - //this will reset every time we make a change - let code = ''; - - const currComponent = state.components.find( - (element) => element.id === state.canvasFocus.componentId - ); - - //writes each component to the code - componentBuilder(currComponent.children).forEach((element) => { - try { - code += ReactDOMServer.renderToString(element); - } catch { - return; - } - }); - - //writes our stylesheet from state to the code - code += ``; - - //adds the code into the iframe - useEffect(() => { - //load the current state code when the iframe is loaded and when code changes - iframe.current.addEventListener('load', () => { - iframe.current.contentWindow.postMessage(code, '*'); - }); - iframe.current.contentWindow.postMessage(code, '*'); - }, [code]); - - return ( - <> -
    -