Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/development/style-guide/frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
# OpenProject development style guide - frontend

Working on the frontend day to day — dev server, builds, plugin linking and writing specs — is documented in [frontend/doc](https://github.com/opf/openproject/tree/dev/frontend/doc).

## Code format

OpenProject follows the [AirBnB's style guide](https://github.com/airbnb/javascript) regarding to the code format.
Code format is enforced by ESLint. The house rules live in `frontend/eslint.config.mjs`, largely through [`@stylistic`](https://eslint.style/), and indentation defers to `.editorconfig`.

```shell
cd frontend && npx eslint src/
```

## Development patterns

> The patterns below describe the Angular application, which is now legacy. New development uses Hotwire — Turbo and Stimulus — with server-rendered HTML and Primer via ViewComponent. Reframing this section is tracked in [DREAM-823](https://community.openproject.org/wp/DREAM-823).

### Angularized

OpenProject follows the [Angular's style guide](https://angular.io/guide/styleguide) patterns.
OpenProject follows the [Angular's style guide](https://angular.dev/style-guide) patterns.

### Declarative

Expand Down
2 changes: 2 additions & 0 deletions docs/development/testing/running-tests-locally/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ CI additionally typechecks the application and spec sources with `tsc`, which re
npm run typecheck
```

For guidance on writing frontend specs — the Stimulus test helpers, Testing Library queries and coverage — see [frontend/doc/TESTING.md](https://github.com/opf/openproject/blob/dev/frontend/doc/TESTING.md).

## Unit tests

After following the prerequisites, use the following command to run individual specs:
Expand Down
51 changes: 19 additions & 32 deletions frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,36 @@

## Directory Structure

- `./src/` - Frontend code
- `./src/app/` - Legacy Angular modules/components
- `./src/common/` - Framework-agnostic modules (the `core-common` alias), importable from both Angular and Stimulus. Code belongs here when it depends on neither framework and both sides need it; a helper only Stimulus controllers use belongs in `./src/stimulus/helpers/` instead.
- `./src/stimulus/` - Stimulus controllers
- `./src/turbo/` - Turbo integration
- [Sortable lists selection contract](src/stimulus/controllers/dynamic/sortable-lists/README.md) - Consumer wiring, item identity, selection gestures, and presentation.
- `./src/app/` — legacy Angular modules and components
- `./src/common/` — framework-agnostic modules (the `core-common` alias)
- `./src/stimulus/` — Stimulus controllers
- `./src/turbo/` — Turbo integration

## Configuration Files

- `eslint.config.mjs` - JavaScript/TypeScript linting
- `../package.json` / `./frontend/package.json` - Node.js dependencies
Where a given module belongs is explained in [doc/README.md](./doc/README.md#directory-structure).

## Version Requirements

- Node: `^24.15.0` (see `package.json` engines)
- Node: `^24.15.0` (see `engines` in the repository root `../package.json`)

## Setup
## Commands

```bash
npm ci && cd .. # Install Node packages
npm ci # install packages
npx eslint src/ # lint
npm run typecheck # typecheck application and spec sources
npm test # run the specs
```

## Code Style

### JavaScript/TypeScript
- **New development**: Use Hotwire (Turbo + Stimulus) with server-rendered HTML
- **Legacy code**: Follow ESLint rules
- Prefer TypeScript over JavaScript
- Use [Primer Design System](https://primer.style/product/) via ViewComponent

## Linting
New development uses Hotwire (Turbo + Stimulus) with server-rendered HTML and [Primer](https://primer.style/product/) via ViewComponent; the Angular tree is legacy. Prefer TypeScript over JavaScript.

```bash
# JavaScript/TypeScript
npx eslint src/
Patterns and formatting conventions: [frontend style guide](../docs/development/style-guide/frontend/README.md).
Components and design system: [design system](../docs/development/design-system/README.md).

# Typecheck application and spec sources
npm run typecheck
```
## Further documentation

## Testing

```bash
# Frontend (Jasmine/Karma)
npm test && cd ..
```
- [doc/TESTING.md](./doc/TESTING.md) — writing frontend specs, the Stimulus helpers, coverage
- [doc/PLUGINS.md](./doc/PLUGINS.md) — linking plugin frontends during development
- [doc/README.md](./doc/README.md) — dev server, builds, dependencies
- [Sortable lists selection contract](src/stimulus/controllers/dynamic/sortable-lists/README.md) — consumer wiring, item identity, selection gestures, and presentation
76 changes: 43 additions & 33 deletions frontend/doc/PLUGINS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
Rails plugins with Frontends
====================

OpenProject Community edition has some plugins that contain a frontend,
e.g., [Costs](https://github.com/finnlabs/openproject-costs/) or [My Project Page](https://github.com/finnlabs/openproject-my_project_page/).
Some OpenProject modules ship a frontend of their own. Costs is the worked example below; it lives in this repository at `modules/costs` and is loaded from `Gemfile.modules`, not cloned separately.

For developing these plugins, they need to be linked so either the Legacy or Angular frontend can see and process them.

Expand All @@ -12,17 +11,21 @@ For developing these plugins, they need to be linked so either the Legacy or Ang



To install a plugin, you clone it locally and place it into your `Gemfile.plugins` like so:
Modules bundled with OpenProject are already listed in `Gemfile.modules` and need no installation step:

```ruby
gem 'costs', path: 'modules/costs'
```

A plugin developed outside this repository is added to `Gemfile.plugins` instead, pointing at wherever you cloned it:

```ruby
group :opf_plugins do
gem 'openproject-costs', path: '../plugins/openproject-costs'
gem 'openproject-my-plugin', path: '../plugins/openproject-my-plugin'
end
```



After that you first need to bundle the application with `bundle install`.
After changing either file, run `bundle install`.

The plugin is now known in the OpenProject application, but their frontends are not linked. For development, before you run any frontend build or `CLI` commands, execute this rake task:

Expand All @@ -34,13 +37,13 @@ The plugin is now known in the OpenProject application, but their frontends are



This will ensure those plugins with a frontend are symlinked to `frontend/src/app/modules/plugins/linked/` for plugins with an exported Angular module under `frontend/module/main.ts`.
This will ensure those plugins with a frontend are symlinked to `frontend/src/app/features/plugins/linked/` for plugins with an exported Angular module under `frontend/module/main.ts`.



### Example: OpenProject Costs plugin
### Example: the Costs module

The [Costs](https://github.com/finnlabs/openproject-costs/) plugin has both legacy components that are still used by Rails templates as well as an entry module file to register to the Angular frontend.
The Costs module (`modules/costs`) has both legacy components that are still used by Rails templates as well as an entry module file to register to the Angular frontend.

Let's take a look at the file structure of the costs folder `frontend/`:

Expand All @@ -55,32 +58,31 @@ module
The Angular frontend entry point is `frontend/module/main.ts` and should export a `PluginModule` ngModule that looks like the following:

```typescript
import { NgModule } from '@angular/core';
import { OpenProjectPluginContext } from 'core-app/features/plugins/plugin-context';
import { WorkPackageAction } from 'core-app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service';

export function initializeCostsPlugin() {
window.OpenProject.getPluginContext()
.then((pluginContext:OpenProjectPluginContext) => {
// Register a field type to the core EditField functionality
pluginContext.services.editField.extendFieldType('select', ['Budget']);

// Register a hook callback for a specific core hook
pluginContext.hooks.workPackageSingleContextMenu(function(params:any) {
return {
key: 'log_costs',
icon: 'icon-projects',
indexBy: function(actions:any) {
var index = _.findIndex(actions, {key: 'log_time'});
return index !== -1 ? index + 1 : actions.length;
},
resource: 'workPackage',
link: 'logCosts'
};
});
});
window.OpenProject.getPluginContext()
.then((pluginContext:OpenProjectPluginContext) => {
// Register a field type to the core EditField functionality
Comment thread
myabc marked this conversation as resolved.
pluginContext.services.editField.extendFieldType('select', ['Budget']);

// Register a hook callback for a specific core hook
pluginContext.hooks.workPackageSingleContextMenu(():WorkPackageAction => ({
key: 'log_costs',
icon: 'icon-projects',
indexBy: (actions:WorkPackageAction[]) => {
const index = actions.findIndex((action) => action.key === 'log_time');
return index !== -1 ? index + 1 : actions.length;
},
link: 'logCosts',
}));
});
}


@NgModule({
providers: [
],
providers: [],
})
export class PluginModule { // The name PluginModule is important!
constructor() {
Expand All @@ -89,6 +91,14 @@ export class PluginModule { // The name PluginModule is important!
}
```

`workPackageSingleContextMenu` is invoked without arguments, so its callback takes none. `indexBy`
receives the actions collected so far and returns the position the new entry should take.

`hooks` is typed as `Record<string, (callback:(...args:any[]) => unknown) => void>`, so a callback
returning anything at all compiles. Annotating the return as `WorkPackageAction` is what makes a
wrong key an error rather than a silent no-op — the consuming code casts the collected results to
`WorkPackageAction[]` regardless.

A working implementation of this pattern lives at `modules/costs/frontend/module/main.ts`.

The rake task will generate a Module under `frontend/src/app/modules/plugins/linked-plugin-module.ts` that will import all these plugin modules. This happens by filling an ERB template by the rake task and is performed in `lib/open_project/plugins/frontend_linking/*`
The rake task will generate a module under `frontend/src/app/features/plugins/linked-plugins.module.ts` that will import all these plugin modules. This happens by filling an ERB template by the rake task and is performed in `lib/open_project/plugins/frontend_linking/*`
57 changes: 30 additions & 27 deletions frontend/doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,57 @@

The OpenProject frontend is located at `frontend/src` and uses the Angular CLI (which itself uses Vite in dev and esbuild in production) to compile and serve locally.

## Directory structure

- `src/app/` — the legacy Angular application. New work does not go here.
- `src/common/` — framework-agnostic modules, reachable through the `core-common` alias and importable from both Angular and Stimulus. Code belongs here when it depends on neither framework and both sides need it; a helper only Stimulus controllers use belongs in `src/stimulus/helpers/` instead.
- `src/stimulus/` — Stimulus controllers, the current approach for new behaviour.
- `src/turbo/` — Turbo integration: stream actions, custom elements and helpers.

## Angular frontend

When developing, `npm run serve` will open the Angular CLI dev server (Vite) as a proxy that serves assets from memory.
That server is running on `http://localhost:4200` by default and will forward all requests it cannot handle to the Rails server
which it expects to run at `http://localhost:3000`.
When developing, `npm run serve` starts the Angular CLI dev server (Vite) on `http://localhost:4200`, serving frontend assets from memory. It is not a general-purpose proxy: it forwards only `/api` and `/assets/frontend/media` to Rails, as configured in `frontend/src/proxy.conf.mjs`.

You can always access the Rails server with `http://localhost:3000`
and it will forward the requests to the CLI Proxy unless an empty ENV variable `OPENPROJECT_CLI_PROXY='''` is passed to it.
Application pages are reached through Rails at `http://localhost:3000`, which forwards frontend asset requests to the dev server unless `OPENPROJECT_CLI_PROXY=''` is passed to it.

Then, Rails will try to locate the asset on disk (e.g., as output from the `rake assets:precompile` task).
This is also what happens in production mode.

To learn more about how this behavior works in detail, see the asset helper at `app/helpers/frontend_asset_helper.rb`.

### Ahead-of-Time compilation (AOT)

In development, by default AOT is disabled. You can force it in by running `npm run serve --aot`.
For production builds with `ng build --prod`, `--aot` is enabled by default as per the `frontend/angular.json` configuration.

### Production builds

Production builds can be triggered either through the `rake assets:precompile` rake task (which will compile legacy and angular frontend)
or by running `npm build --prod` manually.
Production builds are triggered either through the `rake assets:precompile` rake task, which compiles
the frontend along with the rest of the assets, or by running `npm run build` manually. That script
builds with `--configuration production`, which enables ahead-of-time compilation.

## Tests

Tests are run with karma-jasmine through the Angular CLI `ng test`. To watch the test output, use `ng test --watch`.
Only files ending with `.spec.ts` will be matched and compiled.
Tests run with `npm run test`, which uses the Angular CLI's `ng test` to build the specs and
[Vitest](https://vitest.dev/) to execute them in a real browser. Use `npm run test:watch` to watch.
Only files ending with `.spec.ts` are matched.

For more information, see [TESTING](./TESTING.md).

## Plugins

OpenProject Community edition has some plugins that contain a frontend,
e.g., [Costs](https://github.com/finnlabs/openproject-costs/)
or [My Project Page](https://github.com/finnlabs/openproject-my_project_page/).
e.g., Costs, which lives in this repository at `modules/costs`.

For developing these plugins, they need to be linked so either the Legacy or Angular frontend can see and process them.
For more information on that part, see [PLUGINS](./PLUGINS.md)



## Living Style Guide
## Components and styling

The style guide is available as part of the Rails development server at: <http://localhost:3000/styleguide>.
Component documentation and previews live in Lookbook, served by the Rails development server at <http://localhost:3000/lookbook> when `lookbook_enabled` is set. See [Design system](https://www.openproject.org/docs/development/design-system/) for Primer and the component library.

For more information on styling the application, see [STYLING](./STYLING.md).
Styles live under `frontend/src`: shared rules in `global_styles`, component styles beside their components, with `styles.scss` and `spot.scss` as the build entry points declared in `frontend/angular.json`.

## Changing or updating Dependencies

We use a `package-lock` to lock down runtime (but not development)
dependencies. When adding or removing dependencies, please use `npm install` to also update the lockfile.
We use a `package-lock` to lock down dependencies, development ones included. When adding or removing dependencies, please use `npm install` to also update the lockfile.
Please commit `package-lock.json` along with any changes to `package.json`.

If you want to install the packages from the lockfile without updating it, please use the following command:
Expand All @@ -64,10 +63,14 @@ npm ci

## Topics

The individual topics for the documentation for the frontend are
Frontend-specific documentation:

1. [TESTING.md](./TESTING.md) — writing frontend specs, the Stimulus helpers, coverage
2. [PLUGINS.md](./PLUGINS.md) — linking plugin frontends during development

Frontend topics documented alongside the rest of the application:

1. `TESTING.md` - documentation of our approach to integration and unit testing
2. `STYLING.md` - notes on styling and the Sass-Pipeline
3. `API.md` - notes on dealing with the several APIs provided by OpenProject
4. `LEGACY.md` - contains additional information on how to use the legacy bundle
5. `PLUGINS.md` - contains additional information on how to link plugins with a frontend during development.
- [Frontend style guide](https://www.openproject.org/docs/development/style-guide/frontend/) — code format and development patterns
- [Design system](https://www.openproject.org/docs/development/design-system/) — Primer and the component Lookbook
- [Running tests locally](https://www.openproject.org/docs/development/testing/running-tests-locally/#frontend-tests) — how to invoke the suite
- [Testing architecture](https://www.openproject.org/docs/development/testing/) — where frontend specs sit in the wider strategy
29 changes: 0 additions & 29 deletions frontend/doc/STYLING.md

This file was deleted.

Loading
Loading