# Roomle UI

This project provides the UIs on top of the Roomle Web SDK (https://github.com/Roomle/web-sdk). It will be constantly developed and maintained by the Roomle Web Team since it's the official user interface of the Roomle web components and standalone version (e.g: https://www.roomle.com/t/configurator/).

Which UIs are in this repository:

* Configurator
* ~~Planner~~

Techstack:

* Vue (Vuex)
* Typescript
* ~~Jest~~ Vitest
* ~~Cypress~~ Playwright
* Babel

## Before your first commit

This repository uses conventional commits: https://www.conventionalcommits.org/en/v1.0.0/

At least read through the summary before committing: https://www.conventionalcommits.org/en/v1.0.0/#summary

Be careful:

* Commit types start with a lower case letter (e.g fix:)
* Commit messages start also with a lower case letter

For example: `fix: add missing file to project`

## Project setup

`yarn install`

**Compiles and hot-reloads for development:** `yarn run serve`

**Compiles and minifies for production** `yarn run build`

**Run your tests** `yarn run test`

**Lints and fixes files** `yarn run lint`

**Run your end-to-end tests** `yarn run test:e2e`

**Run your unit tests** `yarn run test:unit`

## Project structure

* /public
  * /translations
* /src
  * /common
    * /assets
    * /components
  * /configurator
    * /assets
    * /components
    * /store
  * ~~/planner~~

### Public directory (/public)

These assets will simply be copied and will not go through webpack. Need to be referenced via absolute paths.

#### Translations (/public/translations)

Default folder for LocalTranslationSource (src/common/translations/local-translation-source.ts)

### Source directory (/src)

Consider to read the Vue style guide first before changing files in those directories: https://vuejs.org/v2/style-guide/

#### Common (/src/common)

**Contains all components common to all UIs.**

#### Configurator (/src/configurator)

All files related **only** to the configurator, see https://www.roomle.com/t/configurator/

**Assets (/src/\*/assets)**

Files placed here need to be imported in JavaScript or referenced in templates/CSS via relative paths. Such references will be handled by webpack.

**Components (/src/\*/components)**

This directory contains all Vue single component files (https://vuejs.org/v2/guide/components.html).

**Vuex Store (/src/\*/store)**

Contains all files related to Vuex: https://vuex.vuejs.org/guide/

## Guidelines

### Store

Only access the store from templates in a reading manner. Never dispatch actions or commit mutations from within the template.

If something is local to only this specific component it's ok to store this state inside the components instance but always be aware that we need to be able to hot-swap components which means that a component for mobile UI could be replace by a component for desktop UI but the hot swapped component needs to know where the other component stopped and from where to continue. Therefore there are only some cases where we do not need to store the state in the global state tree.

We try to limit boilerplate code therefore we want to be lean on the distinction between a mutation and an action. As a general rule of thumb we decided to do simple state changes in mutations. As soon as complexity grows we create actions. Example for when to use actions:

* we need to change multiple properties of the store
* we need to change the store in an async way

### Code Style

When writing code, it is important to adhere to consistent style guidelines to ensure readability and maintainability. The following section outlines the recommended code style conventions to be followed in this project.

#### Components

When writing/refactoring components with composition API syntax, we use

```html
<script setup lang="ts">
  ...
</script>
```

to contain all component logic, and we stick to the following order of sections with labels included:

```ts
import type { StoreState } from '@/common/store';
// Async components
const EmbeddedAdditionalInfo = defineAsyncComponent(
  () => import('@/common/components/EmbeddedAdditionalInfo.vue'),
);

// Dependencies
const store = useStore<StoreState>();

// Props
const props = defineProps<{ isTrue: boolean; isOptional?: boolean }>();

// Data
const isToggled = ref(false);

// Computed properties
const isSomeFlag = computed(() => {
  return isToggled.value === props.isOptional;
});

// Methods
const save = async () => rapiAccess.save();

// Hooks
onMounted(() => {
  // do some stuff
});

// Initialization
isToggled.value = doSomeStuffAndCalculateThings();
```

#### Constants

In case of using constants to define a set of named values, it is good practice to use singular names instead of plural names. This convention helps to maintain clarity and consistency throughout the codebase. It is also important to use `const` instead of `enum` to create a type, for example:

```
export const INTERACTION_VIEWS_TYPE = {
  PARTLIST: 'rubens_partlist',
  MATERIAL_INFO: 'material_info',
  SIDEBAR: 'rubens_sidebar',
} as const;
```

If you want to type a property as a value of this constant, you can use `Values<typeof INTERACTION_VIEWS_TYPE>`

### Husky

[Husky](https://typicode.github.io/husky/) is a tool that helps developers work with Git hooks more efficiently and run all the scripts that need to work at various stages.

By simplifying the process of setting up Git hooks, developers can create effective solutions faster. Husky works within the package.json file by including an object that configures Husky to run certain scripts, and then Husky manages the script at specific points in the Git lifecycle.

Husky is a dotnet tool that allows you to run arbitrary actions on specific Git hooks. These could be to enforce the same sort of things that you would do centrally, but here they would run before committing code into Git. For example, you could ensure your code built and passed all unit tests.

In order to activate Husky in your local repo you have to run this command just one time `npm run prepare`. This command will allow Git hooks to be enabled.

**lint-staged**

Every time you push a new change, it gets validated from Husky and runs a lint command but if you run the lint for the whole project every push, it will take time, that is why we install `lint-staged` and it works along with Husky so if you have staged changes, it will run the lint for those changes only and not for the whole project.

**Conventional commits validations**

We also include validation for checking the format of the commit message against [conventional commit](https://www.conventionalcommits.org/) format so please be sure that you write a valid commit message that agrees with conventional commit standards. As an exception to these rules, we allow the standard commit messages from tools like https://git-fork.com/ and https://www.sourcetreeapp.com/ when merging branches.

### Integration with Git clients

Sometimes there are troubles when integrating with Git clients like SourceTree. To find possible ways to resolve those issues follow the link: https://github.com/typicode/husky/issues/904

For Georg the following worked:

```
echo "export PATH=":/usr/local/bin/:$PATH"" > ~/.huskyrc
```

## CI/CD

### GitHub Actions Preview on cloud storage bucket

* Create bucket
* Authenticate using WIF, edit and run `./.github/workflows/setup-wif.sh`
* Assign `Storage Legacy Object Viewer` for all users to allow access
* Assign `Storage Object Admin` for service account created for WIF
* Configure CDN to serve content as website. On the bucket overview, click on the 3 vertical dots. Edit start and error page settings.

### GitHub Actions build and deploy to cloud storage bucket

The workflow is defined in `.github/workflows/cd-gcp.yaml`.

* it is split into multiple jobs
  * Build
  * Deploy to \[dev|test|alpha|live]
  * Publish docs artifact to \[dev|test|alpha|live]

The same workflow is used to deploy to all stages. We use GitHub Environments specify which branch name should trigger a job: https://github.com/roomle-dev/roomle-ui/settings/environments. See `environment:` for details. This also allows to add a url to the github workflow visualisation.

To handle special cases inside like `building for test, building for alpha` or `release to npmjs` we use `if` conditions to limit to certain branch names inside the re-used `build` job.

Errors show up when the env protection rule gets broken. An example error would be `Branch "master" is not allowed to deploy to alpha due to environment protection rules.` To avoid such errors on successful builds we use `if` conditions where a `environment` is defined.

#### Dependencies / Integration

**Action secrets**

Added `GH_TOKEN`, `NPM_TOKEN`, `SONARQUBE_TOKEN` as github actions secrets at: https://github.com/roomle-dev/roomle-ui/settings/secrets/actions

The secrets `SLACK_WEBHOOK_URL` and `SLACK_WEBHOOK_URL_LIVE` contain a webhook url to send Slack notifications to a specific channel. You can manage the webhook urls in the [GitHub Action Slack app](https://api.slack.com/apps/A04FPG9SDK2/incoming-webhooks?success=1)

**GCP service account**

Created a service account (+permissions) to authenticate from github to google cloud. This allows us to sync the app and the documentation build artifact to cloud storage buckets where the service account has access. You can run the commands using `./.github/workflows/setup-wif-gh-gcp-bucket-staging.sh`

**GitHub job permissions**

To use WIF requires overwriting the github default permission. Check out `permissions:`.

* https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs#example-assigning-permissions-to-github\_token
* We set permissions on step level, to avoid conflicts between steps (upload-to-bucket vs. semantic-release)

**Forwarding rule on load balancer**

To deliver the application on a specific url, we have to set a forward rule on the load balancer.

* i.e. `https://dev.roomle.com/t/cp-cdn/` should redirect traffic to bucket `gs://roomle-configurator-dev`
* Can be managed in: https://console.cloud.google.com/net-services/loadbalancing/edit/http/roomle-loadbalancer-1-test?project=roomle-01

**GitHub actions build artifact handover**

Every GitHub Actions job starts with an empty container image. To use a build artifact from a previous job, we have to upload and download as a github artifact.

* Exchange data between several build jobs within a workflow
* There are several options as: `wildcard`, `dir`, `files`. For details see: https://github.com/actions/upload-artifact

### Run GitHub actions locally

Create a new env file `roomle-build.secrets` in the project's `.act` directory and add the environment variables used in the github action.

```
SSH_HOST="github.com"
SSH_PRIVATE_KEY="-----PRIVATE KEY FROM PASSWORD MANAGER-----"
GOOGLE_BUCKET_CREDENTIALS='{"value": "json-key-file","source": "KEY FILE FROM PASSWORD MANAGER"}'
```

Install `act` to run the github action without pushing it to the repository.

```
act -j mirror-master-branch --secret-file .act/roomle-build.secrets
act -j mirror-live-branch --secret-file .act/roomle-build.secrets
## on the mirrored repo: https://github.com/roomle/roomle-ui-mirrored
act -j mirror-master-update-docs --secret-file .act/roomle-build.secrets
act -j mirror-live-update-docs --secret-file .act/roomle-build.secrets
act --container-architecture linux/amd64 -j mirror-master-branch -e .act/cd-mirror-master-branch.json --secret-file .act/roomle-build.secrets
```

```
# create pull request for dependency upgrade
# - failed and could get it running because of git@***: Permission denied (publickey).
act --container-architecture linux/amd64 -j dependency-upgrade-web-sdk -e .act/ci-dependency-upgrade.json --secret-file .act/roomle-build.secrets

act --container-architecture linux/amd64 -j dependency-upgrade-test --secret-file .act/websdk.secrets
```

### Google Cloud Build (deactivated)

* Create a new ssh key. For details see: https://cloud.google.com/build/docs/access-github-from-build
* Add the `PUBLIC` key as deploy key to the repository `https://github.com/roomle-dev/roomle-ui/settings/keys`. Name it i.e. `SSH-Google-Cloud-Build-496816138068`.
* Add the `PRIVATE` ssh key as a secret to the `Google Secret Manager`.
* Update the webhook trigger in GCP project `rml-ci-cd`. Run `./cloudbuild/upsert-triggers.sh` using the correct gcloud user and configuration.
* Copy the webhook url from the trigger at `https://console.cloud.google.com/cloud-build/triggers;region=global/edit/03927c71-2af1-4bf7-bbd0-89ad1f9cb3cd?project=rml-ci-cd` and paste it to a webhook in GitHub `https://github.com/roomle-dev/roomle-ui/settings/hooks`

Alternatively we could use the Google Cloud Build GitHub App. For details see: https://cloud.google.com/blog/products/devops-sre/cloud-build-brings-advanced-cicd-capabilities-to-github

### Unit tests trigger workflow

To optimize GitHub build times, we've made the unit test triggering in pull requests more dynamic:

* **Initial PR Testing:** When a pull request is created, only tests relevant to the changes in the pull request are run. This is achieved using Jest's `--changedSince` flag.
* **Pre-Merge the pull request:** After the pull request is reviewed and ready to be merged, a different workflow is triggered to run all unit tests. This ensures the master branch remains stable after merging the pull request.
* **Skipping Tests for minor fixes:** If the pull request contains a minor fix (e.g., a simple CSS adjustment) and the developer is confident it doesn't need to run the whole unit tests workflow, the approval workflow can be skipped. To do so, prefix your commit message with `fix(small): [YOUR FIX COMMIT MESSAGE]`. The workflow will recognize this prefix and won't trigger tests upon the pull request approval.

### Playwright

For E2E tests we use https://playwright.dev/. Please bear in mind that an E2E test is expensive (computation resources etc.), therefore please use it only in cases where it makes sense. Also, make sure that requests to our backend are mocked. We do not want to DDoS our own service with our tests. To mock our backend we use HAR files. For a quick intro to HAR files see the docs here: https://playwright.dev/docs/network#replaying-from-har

The default way to run our tests is with fully mocked network connection. This is useful because:

* we do not want to stress test our backend
* we do not want to create backend costs (traffic, computation etc)
* we want to be able to test several edge cases that won't happen with the real backend

\~\~Since we run our E2E tests before our app is deployed somewhere we have to do some shenanigans with the HAR file. First of all load the content in the browser and record the network activity inside the dev-tools. Then filter the network requests by our RAPI-Url. Currently only Firefox behaves correctly (in Chrome you have to save every request and in Safari OPTIONS calls are missing). You can find a screencast here: https://roomle.atlassian.net/wiki/spaces/DT/pages/2272722984/Playwright+HAR+file+generation \~\~

The above explained behavior is not needed anymore. Please have a look at `tests-e2e/specs/example.spec.ts` to see how it is used now. There is a [fixture](https://playwright.dev/docs/test-fixtures) called `pageInteractor` that can be used to navigate to a page and mock the network automatically.

To interact with the network mock there are currently 3 modes:

* skip: means, do not mock the network and query the backend
* update: means, record network activity and write it into HAR files
* enforce: means, read requests from recoreded HAR files and report if one call was not mocked

Playwright is a very powerful tool and probably too powerful. There are several things that help creating tests. Like [codegen](https://playwright.dev/docs/codegen) and the `--ui` mode. Nevertheless it's complex and some problems are not directly obvious. Therefore there is some kind of learning curve and I welcome everyone to start the learning journey before ditching Playwright immediatelly. There is plenty of learning material, the docs are awesome and there is a huge community so I think it should be possible to get quickly up to speed.

If you are trying to work with screenshots and snapshots it is important that you generate the screenshots also on Linux. Luckily Microsoft provides a Docker Image that can help us. This is what they wrote in the docs, for more details see: https://playwright.dev/docs/test-snapshots

```
# If you are not on the same operating system as your CI system, you can use Docker to generate/update the screenshots:

docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.46.0-jammy /bin/bash
npm ci
export CI=1
npm run test:e2e -- --update-snapshots
```

Side note: to run just one test do something like:

```
npx playwright test embedding.spec.ts -g "forward global callbacks" --update-snapshots
```

This helps update snapshots and missing HAR stuff. Otherwise, we always update everything.

If you run into troubles that `click` events do not come in correctly, probably you need to send the `click` calls simultaneously. For more details see here: https://tally-b.medium.com/the-illustrated-guide-to-using-promise-all-in-playwright-tests-af7a98af3f32

In our case this happens when the "mode buttons" collapse on mobile. The following PR describes teh fix: https://github.com/roomle-dev/roomle-ui/pull/292

To debug the test runs on CI download the trace from Github and run:

```
npx playwright show-trace 'path/to/the/trace-zip.zip'
```

A more sophisticated wrap-up can be found here: https://roomle.atlassian.net/wiki/spaces/DT/pages/2623143943/Playwright+reintroduction+2024

That's it, Happy E2E testing 🥳

If you are using VSCode you can also follow the tutorial on how to use the VSCode plugin: https://playwright.dev/docs/getting-started-vscode

### Creating Translations

A guide for creating translations with [localise.biz](https://localise.biz) and [DeepL.com](https://deepl.com) is available on this confluence page here: https://roomle.atlassian.net/wiki/x/EQCIm, remember to obtain a localise.biz API key and set it to `LOCO_KEY` in your `.env.local`.

### Important Note on Partlist Prices

When using `usePriceService=true`, zero prices will no longer be shown in the partlist by default. However, you can enable the display of the price column for debugging purposes by using the `debug=true` flag.
