Skip to content

Excel Editor SPA - Study of a complete application, with a SPA (Angular) and an API (Symfony), which allows users to connect, and to be able, according to their rights, to import Excel data and modify them online.

License

Notifications You must be signed in to change notification settings

jprivet-dev/excel-editor-spa

Repository files navigation

Excel Editor SPA

Project release Angular release Codacy code quality Codacy code quality

Summary

1. Presentation

1.1. SPA, API & Insomnia

Study of a complete application, with a SPA (Angular) and an API (Symfony), which allows users to connect, and to be able, according to their rights, to import Excel data and modify them online.

Excel Editor SPA

https://github.com/jprivet-dev/excel-editor-spa

Excel Editor API

https://github.com/jprivet-dev/excel-editor-api

Excel Editor Insomnia

https://github.com/jprivet-dev/excel-editor-insomnia

1.2. Diagrams

1.2.1. Global architecture

excel editor architecture diagram

1.2.2. Authentication & JWT

excel editor spa auth diagram

2. Prerequisites

2.1. Docker Compose CLI

Be sure to install the latest version of Docker Compose CLI plugin.

3. Installation

3.1. To the very first cloning of the project

  1. $ git clone git@github.com:jprivet-dev/excel-editor-spa.

  2. $ cd excel-editor-spa.

  3. $ make install: This command will execute the following commands:

    • $ make build: Build or rebuild fresh images if necessary.

    • $ make start: Create and start containers (alias: $ make up).

  4. Open your browser on http://localhost:4200.

⚠️
To use Excel Editor SPA, you will need to install also Excel Editor API, an API made with Symfony.

3.2. The following times

  1. Just launch the project with $ make start command.

  2. Open your browser on http://localhost:4200.

💡
  • $ make stop: Stop and remove containers, networks (alias: $ make down).

  • $ make: See all available make commands.

4. Screenshots

4.1. Login page

screenshot login page

4.2. Admin (Upload, Read, Write & delete)

4.2.1. No data

screenshot admin data empty

4.2.2. Upload (choose a file)

screenshot admin data dialog upload

4.2.3. Upload (results)

screenshot admin data dialog upload results

4.2.4. Read (table with data)

screenshot admin data

4.2.5. Write (edit)

screenshot admin data dialog edit

4.2.6. Write (create)

screenshot admin data dialog create

4.2.7. Delete

screenshot admin data dialog delete

4.3. User (Read only)

screenshot user data

4.4. Loading (Progress bar & Spinner)

screenshot anim

5. Upload Excel files

The Excel files are uploaded and renamed (with a unique indentifier) in the uploads folder of the Excel Editor API.

💡
You can test and upload the Excel files in the data folder of the Excel Editor API.

6. Main technical constraints for the study

  • Use of the latest version of Angular.

  • No NgRx Store: the objective is to study in depth the observable data services and principles.

  • Only Angular Material UI: no merge with Bootstrap, tailwindcss, or other CSS frameworks.

  • Use mainly the code generation commands (ng generate).

  • The project must be dockerized.

  • The project must have a consistent and correct code coverage.

  • The data imported from the excel file are in French: this force us to dissociate the specific language of the data (in French) from the "technical" language of the framework (in English).

7. Style Guide

7.1. Angular coding style guide

7.2. JSON naming convention

That project (API & SPA) use the camelCase format for the property names of JSON responses:

{
  "thisPropertyIsAnIdentifier": "identifier value"
}

8. Codacy configuration

8.1. Code coverage

Duplicate CODACY_PROJECT_TOKEN.sh:

$ cp scripts/CODACY_PROJECT_TOKEN.sh.dist scripts/CODACY_PROJECT_TOKEN.sh

And define the API token CODACY_PROJECT_TOKEN (see https://app.codacy.com/gh/jprivet-dev/excel-editor-spa/settings/coverage).

The file scripts/CODACY_PROJECT_TOKEN.sh is ignored by Git and imported by scripts/reporter.sh.

The file scripts/reporter.sh generates code coverage (a lcov.info with Instanbul) and uploads the coverage reports on Codacy.

💡
Karma: generate code coverage using Istanbul.

8.2. ESLint

Codacy scans the ESLint configuration in the .eslintrc.json file in this repository root.

💡
PHPStorm can also use ESLint. See Configure ESLint.

8.3. Stylelint

Codacy scans the Stylelint configuration in the .stylelintrc file in this repository root.

We need to install stylelint-config-standard-scss:

$ npm install --save-dev stylelint stylelint-config-standard-scss

And create a .stylelintrc.json configuration file:

{
  "extends": "stylelint-config-standard-scss"
}

Use the $ make stylelint command to execute Stylelint on the src folder.

💡
PHPStorm can also use Stylelint. See Configure Stylelint.

9. PHPStorm configuration

The following configuration are provided for PHPStorm 2022.3.1

9.1. Configure a remote Node.js interpreter

  1. Go in Settings (Ctrl+Alt+S) > Languages & Frameworks > Node.js.

  2. In Node interpreter, click on …​ and Add Remote…​.

  3. In the Configure Node.js Remote Interpreter window, choose excel-editor-spa-node:latest and click on OK.

  4. In the Settings window, click on OK.

phpstorm settings node remote interpreter
phpstorm settings node

9.2. Configure ESLint

🔥
Before you start: Configure a remote Node.js interpreter.

Configure in Settings (Ctrl+Alt+S) > Languages & Frameworks > JavaScript > Code Quality Tools > ESLint :

phpstorm settings eslint

After the configuration, you can see the ESLint alerts in your code. For example:

phpstorm settings eslint error
⚠️

If you have this error: ESLint: Can’t run process: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: \"node\": executable file not found in $PATH: unknown:

phpstorm settings eslint fail path unknown

Is that the Node.js interpreter is badly configured. See Configure a remote Node.js interpreter.

9.3. Configure Prettier & Reformat files

🔥
Before you start: Configure a remote Node.js interpreter.

Configure in Settings (Ctrl+Alt+S)> Languages & Frameworks > JavaScript > Prettier :

phpstorm settings prettier

After the configuration, you can reformat your code :

  • With the shortcut Ctrl+Alt+Maj+P.

  • From the contextual menu (Right click > Reformat with Prettier).

phpstorm settings prettier contextual menu
💡
It’s possible to reformat on save.

To reformat on save, Go in Settings (Ctrl+Alt+S)> Languages & Frameworks > JavaScript > Prettier, and check On save option:

phpstorm settings prettier on save

If you click on All actions on save…​, you will see the list of all activated actions:

phpstorm settings tools actions on save
💡
I also use the Optimize import option. This removes unused imports and organizes import statements in the current file. See https://www.jetbrains.com/help/phpstorm/creating-and-optimizing-imports.html#optimize-imports.

9.4. Configure Stylelint

🔥
Before you start: Configure a remote Node.js interpreter.

Configure in Settings (Ctrl+Alt+S)> Languages & Frameworks > JavaScript > Prettier :

phpstorm settings stylelint

For the moment I have an error in PHPStorm with a missing module. I tried to install v8-compile-cache, but nothing works:

phpstorm settings stylelint error module

However, everything works with the $ make stylelint command.
Search still in progress…​

10. Resources & Inspiration

10.1. Standalone components

This project does not yet use completely the standalone components (introduced in Angular 14).

💡
Generate a standalone component:
$ ng g c myComponent --standalone

10.2. Container & Presentational Components

This project is inspired by this pattern (which can quickly become an anti-pattern if applied dogmatically).

What are the main points?

  • Container (or Smart) components:

    • Components that are aware of the service layer (no @Input, no @Output).

    • Top-level components: highest level of components, only for components attached to a route.

    • Examples: UserPage, FollowersSidebar, StoryContainer, FollowedUserList.

  • Presentational components:

    • Components that receive inputs and emit events, nothing else (no services, only @Input and @Ouput).

    • Examples: Sidebar, Story, UserInfo, List

  • A presentational component can contain a container component: it allows for logic for interaction with the service layer to be put deeply into the component tree (if that is where it makes the most sense to have it), also to simplify the intermediate components and avoids code repetition.

In practice its actually much more practical to mix and match the multiple types of component design as we need, and use different types of components at different levels of the tree as necessary - mixing the different features as much as we need.

— ANGULAR UNIVERSITY

10.3. Observable Data Services

This project uses simple stores with Observable Data Services, instead of @ngrx/store.

10.7. Angular Service Layers

10.7.1. When to Use a Store And Why?

You’ll know when you need Flux. If you aren’t sure if you need it, you don’t need it.
— ANGULAR UNIVERSITY

10.7.2. Alternative solutions in the Angular world, other than a store

  • Inject services deep in the component tree.

  • Inject components or services into each other if we feel they are inherently tightly coupled.

  • Create shared data services that might or might not store the data.

10.7.3. Alternative to Redux: MobX

10.10. JWT, Authentication & Permissions

📎
The authentication service of this project is mainly inspired by https://github.com/auth0/auth0-angular.

10.11. Error handling & Best practices

10.11.1. Error on login

API response
{
    "code":401,
    "message":"Invalid credentials."
}
Angular HttpErrorResponse
{
    "headers": {
        "normalizedNames": {},
        "lazyUpdate": null
    },
    "status": 401,
    "statusText": "OK",
    "url": "https://localhost/api/login_check",
    "ok": false,
    "name": "HttpErrorResponse",
    "message": "Http failure response for https://localhost/api/login_check: 401 OK",
    "error": {
        "code": 401,
        "message": "Invalid credentials."
    }
}
Normalized error
{
    "status": 401,
    "name": "HttpErrorResponse",
    "message": "Http failure response for https://localhost/api/login_check: 401 OK",
    "url": "https://localhost/api/login_check",
    "detail": "Invalid credentials."
}

10.11.2. Error on add an existing group

API response
{
    "type": "https://tools.ietf.org/html/rfc2616#section-10",
    "title": "An error occurred",
    "status": 400,
    "detail": "Object(App\\Entity\\Data).nomDuGroupe:\n    The music group \"Nirvana\" already exists. (code 23bd9dbf-6b9b-41cd-a99e-4844bcf3077f)\n",
    "class": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
    "trace": ['...']
}
Angular HttpErrorResponse
{
    "headers": {
        "normalizedNames": {},
        "lazyUpdate": null
    },
    "status": 400,
    "statusText": "OK",
    "url": "https://localhost/api/data",
    "ok": false,
    "name": "HttpErrorResponse",
    "message": "Http failure response for https://localhost/api/data: 400 OK",
    "error": {
        "type": "https://tools.ietf.org/html/rfc2616#section-10",
        "title": "An error occurred",
        "status": 400,
        "detail": "Object(App\\Entity\\Data).nomDuGroupe:\n    The music group \"Nirvana\" already exists. (code 23bd9dbf-6b9b-41cd-a99e-4844bcf3077f)\n",
        "class": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
        "trace": ['...']
    }
}
Normalized error
{
    "status": 400,
    "name": "HttpErrorResponse",
    "message": "Http failure response for https://localhost/api/data: 400 OK",
    "url": "https://localhost/api/data",
    "detail": "Object(App\\Entity\\Data).nomDuGroupe:\n    The music group \"Nirvana\" already exists. (code 23bd9dbf-6b9b-41cd-a99e-4844bcf3077f)\n",
    "exception": "HttpException"
}

10.11.3. Error if the user is not allowed to delete a group

API response
{
    "type": "https://tools.ietf.org/html/rfc2616#section-10",
    "title": "An error occurred",
    "status": 403,
    "detail": "You do not have sufficient rights to delete a music group.",
    "class": "Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException",
    "trace": ['...']
}
Angular HttpErrorResponse
{
    "headers": {
        "normalizedNames": {},
        "lazyUpdate": null
    },
    "status": 403,
    "statusText": "OK",
    "url": "https://localhost/api/data/78",
    "ok": false,
    "name": "HttpErrorResponse",
    "message": "Http failure response for https://localhost/api/data/78: 403 OK",
    "error": {
        "type": "https://tools.ietf.org/html/rfc2616#section-10",
        "title": "An error occurred",
        "status": 403,
        "detail": "You do not have sufficient rights to delete a music group.",
        "class": "Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException",
        "trace": ['...']
    }
}
Normalized error
{
    "status": 403,
    "name": "HttpErrorResponse",
    "message": "Http failure response for https://localhost/api/data/78: 403 OK",
    "url": "https://localhost/api/data/78",
    "detail": "You do not have sufficient rights to delete a music group.",
    "exception": "AccessDeniedHttpException"
}

10.11.4. Throw a simple new Error in a page

With
throw new Error('Parameter is not a number!');
JavaScript Error
Error: Uncaught (in promise): Error: Parameter is not a number!
Error: Parameter is not a number!
    at new PageNotFoundComponent (page-not-found.component.ts:14:11)
    at NodeInjectorFactory.PageNotFoundComponent_Factory [as factory] (page-not-found.component.ts:15:4)
    at getNodeInjectable (core.mjs:3523:44)
    at instantiateRootComponent (core.mjs:12592:23)
    at createRootComponent (core.mjs:14035:23)
    at ComponentFactory.create (core.mjs:13912:17)
    at ViewContainerRef.createComponent (core.mjs:23230:47)
    at RouterOutlet.activateWith (router.mjs:2569:39)
    at ActivateRoutes.activateRoutes (router.mjs:3003:40)
    at router.mjs:2952:18
    at resolvePromise (zone.js:1211:31)
    at resolvePromise (zone.js:1165:17)
    at zone.js:1278:17
    at _ZoneDelegate.invokeTask (zone.js:406:31)
    at Object.onInvokeTask (core.mjs:26261:33)
    at _ZoneDelegate.invokeTask (zone.js:405:60)
    at Zone.runTask (zone.js:178:47)
    at drainMicroTaskQueue (zone.js:585:35)
Normalized error
{
    "name": "Error",
    "message": "Uncaught (in promise): Error: Parameter is not a number!\nError: Parameter is not a number!\n    at new PageNotFoundComponent (http://localhost:4200/main.js:892:15)\n    at NodeInjectorFactory.PageNotFoundComponent_Factory [as factory] (http://localhost:4200/main.js:895:81)\n    at getNodeInjectable (http://localhost:4200/vendor.js:52246:38)\n    at instantiateRootComponent (http://localhost:4200/vendor.js:62953:21)\n    at createRootComponent (http://localhost:4200/vendor.js:64667:21)\n    at ComponentFactory.create (http://localhost:4200/vendor.js:64517:19)\n    at ViewContainerRef.createComponent (http://localhost:4200/vendor.js:75063:43)\n    at RouterOutlet.activateWith (http://localhost:4200/vendor.js:113096:33)\n    at ActivateRoutes.activateRoutes (http://localhost:4200/vendor.js:113679:28)\n    at http://localhost:4200/vendor.js:113625:12"
}

11. Troubleshooting

11.1. [webpack-dev-server] Disconnected!

When I use Slow 3G network conditions:

chrome devtools network conditions

I got the following console error on Chrome:

[webpack-dev-server] Disconnected! index.js:551
[webpack-dev-server] Trying to reconnect...
[webpack-dev-server] Disconnected! index.js:551
[webpack-dev-server] Trying to reconnect...
...

This is a problem when using SSL. See angular/angular-cli#4839.

📎
No problem with Fast 3G.

11.2. Input type "file" isn’t supported by matInput

Whaaaaat !??

ERROR Error: Input type "file" isn't supported by matInput.
💡

Workaround :

<button type="button" mat-raised-button (click)="fileInput.click()">Choose File</button>
<input hidden (change)="onFileSelected()" #fileInput type="file" id="file">
onFileSelected() {
  const inputNode: any = document.querySelector('#file');

  if (typeof (FileReader) !== 'undefined') {
    const reader = new FileReader();

    reader.onload = (e: any) => {
      this.srcResult = e.target.result;
    };

    reader.readAsArrayBuffer(inputNode.files[0]);
  }
}

12. Comments, suggestions?

Feel free to make comments/suggestions to me in the Git issues section.

13. License

"Excel Editor SPA" is released under the MIT License


About

Excel Editor SPA - Study of a complete application, with a SPA (Angular) and an API (Symfony), which allows users to connect, and to be able, according to their rights, to import Excel data and modify them online.

Topics

Resources

License

Stars

Watchers

Forks