diff --git a/docs/dev/web/development/_category_.json b/docs/dev/web/development/_category_.json
index 5bed99fc3..d07b6dbf2 100644
--- a/docs/dev/web/development/_category_.json
+++ b/docs/dev/web/development/_category_.json
@@ -1,9 +1,4 @@
{
"label": "Development",
- "position": 2,
- "link": {
- "type": "generated-index",
- "description": "\uD83D\uDC69\u200D\uD83D\uDCBB Development",
- "slug": "dev/web/development/"
- }
+ "position": 2
}
diff --git a/docs/dev/web/development/index.md b/docs/dev/web/development/index.md
new file mode 100644
index 000000000..4a5586d1c
--- /dev/null
+++ b/docs/dev/web/development/index.md
@@ -0,0 +1,9 @@
+---
+title: 'Development'
+---
+
+This section covers the development workflow and conventions for contributing to OpenCloud Web.
+
+- [Tooling](./tooling) describes the local development setup and the tools used to build OpenCloud Web.
+- [Repository structure and published packages](./repo-structure) explains the most important parts of the Web repository.
+- [Conventions](./conventions) summarizes the coding and contribution conventions used by the project.
diff --git a/docs/dev/web/extension-system/advanced-topics/_category_.json b/docs/dev/web/extension-system/advanced-topics/_category_.json
new file mode 100644
index 000000000..c1aec597f
--- /dev/null
+++ b/docs/dev/web/extension-system/advanced-topics/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Advanced topics",
+ "position": 3
+}
diff --git a/docs/dev/web/extension-system/advanced-topics/configuration.md b/docs/dev/web/extension-system/advanced-topics/configuration.md
new file mode 100644
index 000000000..31436976a
--- /dev/null
+++ b/docs/dev/web/extension-system/advanced-topics/configuration.md
@@ -0,0 +1,57 @@
+---
+title: 'Configuration'
+sidebar_position: 1
+---
+
+This page describes how to add configuration options to your app. Administrators can then change the behaviour of your
+app.
+
+## Manifest and defaults
+
+Put static metadata of your app into `src/manifest.json`. It ends up in the `manifest.json` of your build, together with
+the `name`, `version`, `description`, `license` and `author` fields of your `package.json`, and the entry point of your
+app.
+
+Default values for the app config belong under the `config` key:
+
+```json title="src/manifest.json"
+{
+ "config": {
+ "maxFileSize": 10485760,
+ "showPreview": true
+ }
+}
+```
+
+## Overriding values
+
+There are two ways to override the defaults:
+
+- During development, put your values into `src/config.json`. The file is merged on top of the `config` key of
+ `src/manifest.json`. Don't commit values that only apply to your machine.
+- In a real deployment, administrators override them via the `apps.yaml` file, see the
+ [web applications admin docs](../../../../admin/configuration/web-applications).
+
+```json title="src/config.json"
+{
+ "showPreview": false
+}
+```
+
+## Reading values in your app
+
+The merged config is passed to the `setup` function of your app definition as `applicationConfig`:
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg';
+
+export default defineWebApplication({
+ setup({ applicationConfig }) {
+ const showPreview = applicationConfig?.showPreview ?? true;
+
+ // ...
+ }
+});
+```
+
+Always provide a fallback value. The config can be empty, and an administrator can set any value.
diff --git a/docs/dev/web/extension-system/advanced-topics/index.md b/docs/dev/web/extension-system/advanced-topics/index.md
new file mode 100644
index 000000000..dd2808ae7
--- /dev/null
+++ b/docs/dev/web/extension-system/advanced-topics/index.md
@@ -0,0 +1,10 @@
+---
+title: 'Advanced topics'
+---
+
+This section covers additional concepts for developing and maintaining OpenCloud Web apps.
+
+- [Configuration](./configuration) explains how to define defaults and expose configuration options to administrators.
+- [Styling](./styling) describes how to use the OpenCloud design system, color roles, and Tailwind CSS.
+- [Translations](./translations) explains how to make an app available in multiple languages.
+- [Testing](./testing) introduces the available test setup and helpers.
diff --git a/docs/dev/web/extension-system/advanced-topics/styling.md b/docs/dev/web/extension-system/advanced-topics/styling.md
new file mode 100644
index 000000000..5d53fe175
--- /dev/null
+++ b/docs/dev/web/extension-system/advanced-topics/styling.md
@@ -0,0 +1,59 @@
+---
+title: 'Styling'
+sidebar_position: 2
+---
+
+This page describes how to style your app so that it fits into OpenCloud Web.
+
+## Design system components
+
+The `@opencloud-eu/design-system` package contains the components that OpenCloud Web is built with, for example buttons,
+inputs and modals. Use them where possible. Your app then follows the platform look and inherits accessibility and
+theming for free. See the [design system documentation](../../design-system) for the list of components.
+
+## Color roles
+
+OpenCloud Web exposes its colors as CSS variables with the `--oc-role-` prefix, for example `--oc-role-surface` or
+`--oc-role-on-surface`. Always use these variables instead of fixed color values. They change with the active theme and
+with the color scheme, so your app stays readable in light and dark mode.
+
+```css
+.my-panel {
+ background-color: var(--oc-role-surface-container);
+ color: var(--oc-role-on-surface);
+}
+```
+
+See [the defaults](https://github.com/opencloud-eu/web/blob/main/packages/design-system/src/styles/defaults.css) for a
+complete list of color roles.
+
+## Tailwind CSS
+
+The `@opencloud-eu/extension-sdk` package ships a preconfigured [Tailwind CSS](https://tailwindcss.com/) setup. Import
+its stylesheet in your entrypoint (typically `src/index.ts`) to use it:
+
+```typescript title="src/index.ts"
+import '@opencloud-eu/extension-sdk/tailwind.css';
+```
+
+Tailwind classes need the `ext:` prefix in apps. The prefix avoids style conflicts with the OpenCloud Web runtime.
+
+```html
+...
+```
+
+The setup maps the color roles to Tailwind color utilities, so `--oc-role-surface` is available as `bg-role-surface`,
+`text-role-on-surface` and so on:
+
+```html
+
...
+```
+
+It also sets the OpenCloud font family, a spacing unit of `4px`, the breakpoints `xs`, `sm`, `md`, `lg` and `xl`, and a
+`dark` variant. The `dark` variant follows the color scheme of OpenCloud Web, not the browser setting:
+
+```html
+...
+```
+
+In most cases you do not need the `dark` variant, because the color roles already handle both color schemes.
diff --git a/docs/dev/web/extension-system/advanced-topics/testing.md b/docs/dev/web/extension-system/advanced-topics/testing.md
new file mode 100644
index 000000000..5a8094d4c
--- /dev/null
+++ b/docs/dev/web/extension-system/advanced-topics/testing.md
@@ -0,0 +1,41 @@
+---
+title: 'Testing'
+sidebar_position: 4
+---
+
+This page describes how to test your app.
+
+## Unit tests
+
+The [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) comes with a
+[vitest](https://vitest.dev/) setup. Run the unit tests with:
+
+```bash
+pnpm test:unit
+```
+
+## Test helpers
+
+The `@opencloud-eu/web-test-helpers` package provides utilities for mounting components with a mocked OpenCloud Web
+context. Without them, every component that uses a composable of `web-pkg` fails to mount.
+
+```typescript title="tests/unit/App.spec.ts"
+import { defaultPlugins, mount } from '@opencloud-eu/web-test-helpers';
+import App from '../../src/App.vue';
+
+describe('App', () => {
+ it('renders the title', () => {
+ const wrapper = mount(App, { global: { plugins: [...defaultPlugins()] } });
+ expect(wrapper.text()).toContain('My app');
+ });
+});
+```
+
+For details, please refer to the package's
+[README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-test-helpers/README.md).
+
+## End to end tests
+
+For end to end tests with [Playwright](https://playwright.dev/), the
+[web](https://github.com/opencloud-eu/web) and [web-extensions](https://github.com/opencloud-eu/web-extensions)
+repositories contain working examples.
diff --git a/docs/dev/web/extension-system/advanced-topics/translations.md b/docs/dev/web/extension-system/advanced-topics/translations.md
new file mode 100644
index 000000000..0485a2686
--- /dev/null
+++ b/docs/dev/web/extension-system/advanced-topics/translations.md
@@ -0,0 +1,138 @@
+---
+title: 'Translations'
+sidebar_position: 3
+---
+
+OpenCloud Web uses [gettext](https://www.gnu.org/software/gettext/) for translations, via the
+[vue3-gettext](https://jshmrtn.github.io/vue3-gettext/) library. Your app brings its own translations and hands them to
+the Web runtime. The language that the user picked in OpenCloud is then also used for your app.
+
+Translations are optional. An app without them simply shows its original strings.
+
+## Marking strings
+
+In a script or a composable, get `$gettext` from `useGettext`:
+
+```typescript
+import { useGettext } from 'vue3-gettext';
+
+const { $gettext } = useGettext();
+
+const title = $gettext('Your application name');
+```
+
+In a template, `$gettext` is globally available:
+
+```html
+
+ {{ $gettext('No files here') }}
+
+```
+
+Pass variables as an object. Use the `%{name}` syntax in the string, never string concatenation, because the word order
+changes between languages:
+
+```typescript
+$gettext('Open «%{resource}»', { resource: resource.name });
+```
+
+For plurals, use `$ngettext` with the singular form, the plural form, the count, and the variables:
+
+```typescript
+const { $ngettext } = useGettext();
+
+$ngettext(
+ 'Delete the selected resource?',
+ 'Delete %{amount} selected resources?',
+ resources.length,
+ { amount: resources.length.toString() }
+);
+```
+
+:::warning
+The extraction tool reads your source code, not your runtime values. Always pass a plain string literal to `$gettext`.
+A variable or a template literal cannot be extracted.
+:::
+
+## Setting up the workflow
+
+Add [vue3-gettext](https://jshmrtn.github.io/vue3-gettext/) as a dev-dependency. It ships the `vue-gettext-extract` and
+`vue-gettext-compile` commands.
+
+Create a `gettext.config.cjs` in the root of your app. It defines which files are scanned and which languages you
+support:
+
+```javascript title="gettext.config.cjs"
+module.exports = {
+ input: {
+ path: './src',
+ include: ['**/*.js', '**/*.ts', '**/*.vue']
+ },
+ output: {
+ locales: ['de', 'es', 'fr', 'it'],
+ path: './l10n/locale',
+ potPath: '../template.pot',
+ jsonPath: '../translations.json',
+ flat: false,
+ linguas: false
+ }
+};
+```
+
+Add two scripts to your `package.json`:
+
+```json title="package.json"
+{
+ "scripts": {
+ "l10n:extract": "vue-gettext-extract",
+ "l10n:compile": "vue-gettext-compile"
+ }
+}
+```
+
+`l10n:extract` collects all marked strings into `l10n/template.pot` and creates one `.po` file per language under
+`l10n/locale`. Run it whenever you add or change a string.
+
+`l10n:compile` turns the `.po` files into a single `l10n/translations.json`. Run it after the translated `.po` files
+come back. Commit `translations.json`, because the build imports it.
+
+How the `.po` files get translated is up to you. The OpenCloud repositories use
+[Transifex](https://www.transifex.com/), but any gettext based service or a manual workflow works as well.
+
+## Registering translations
+
+Import `translations.json` and return it from your app definition. The Web runtime merges it into the global
+translations when your app is loaded.
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg';
+import { useGettext } from 'vue3-gettext';
+import translations from '../l10n/translations.json';
+
+export default defineWebApplication({
+ setup() {
+ const { $gettext } = useGettext();
+
+ return {
+ appInfo: {
+ name: $gettext('Your application name'),
+ id: 'your-app'
+ },
+ translations
+ };
+ }
+});
+```
+
+The file is a map of language key to message map:
+
+```json title="l10n/translations.json"
+{
+ "de": {
+ "No files here": "Keine Dateien hier"
+ },
+ "fr": {
+ "No files here": "Aucun fichier ici"
+ }
+}
+```
diff --git a/docs/dev/web/extension-system/build-and-publish.md b/docs/dev/web/extension-system/build-and-publish.md
new file mode 100644
index 000000000..238c63a6c
--- /dev/null
+++ b/docs/dev/web/extension-system/build-and-publish.md
@@ -0,0 +1,104 @@
+---
+title: 'Build and publish'
+sidebar_position: 2
+---
+
+This page describes how to build your app for production and how to publish it in the OpenCloud app store.
+
+## Building an app
+
+Run a production build:
+
+```bash
+pnpm build
+```
+
+The build writes your app into the `dist` folder. Static assets from a `public` folder are copied over as is. The result
+contains:
+
+- `js/-.mjs` - the entry point of your app, plus its chunks.
+- `manifest.json` - the metadata that OpenCloud uses to discover and load your app.
+- any static assets of your app.
+
+The generated `manifest.json` merges your `src/manifest.json` with the `name`, `version`, `description`, `license` and
+`author` fields of your `package.json`. The `entrypoint` key is set by the build.
+
+```json title="dist/manifest.json"
+{
+ "name": "my-app",
+ "version": "1.0.0",
+ "description": "My OpenCloud app",
+ "license": "Apache-2.0",
+ "author": "Me",
+ "entrypoint": "js/my-app-a1b2c3d4.mjs"
+}
+```
+
+## Publishing in the app store
+
+Apps in the OpenCloud app store are listed in the
+[awesome-apps repository](https://github.com/opencloud-eu/awesome-apps). Publishing means adding your app to its
+`webApps/apps.json` file via a pull request.
+
+### Requirements
+
+- Your app must be downloadable as a `.zip` file from a stable URL, for example a GitHub release asset.
+- The zip must contain a `manifest.json` in its root, next to your built files. A production build produces this
+ already, so zip the content of your `dist` folder, not the folder itself.
+
+### apps.json entry
+
+Add one entry under the `apps` key. The schema is defined in the
+[app store types](https://github.com/opencloud-eu/web/blob/main/packages/web-app-app-store/src/types.ts).
+
+```json title="webApps/apps.json"
+{
+ "id": "com.github.my-org.my-repo.my-app",
+ "name": "My App",
+ "subtitle": "One short line about what the app does.",
+ "description": "A longer description.",
+ "license": "Apache-2.0",
+ "versions": [
+ {
+ "version": "1.0.0",
+ "minOpenCloud": "8.0.0",
+ "url": "https://github.com/my-org/my-repo/releases/download/v1.0.0/my-app-1.0.0.zip"
+ }
+ ],
+ "authors": [{ "name": "My Organization", "url": "https://example.org" }],
+ "tags": ["editor", "viewer"],
+ "coverImage": {
+ "url": "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/main/webApps/my-org/my-repo/cover.png"
+ },
+ "screenshots": [
+ {
+ "url": "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/main/webApps/my-org/my-repo/screenshots/1.png",
+ "caption": "What this screenshot shows"
+ }
+ ],
+ "resources": [
+ { "url": "https://github.com/my-org/my-repo", "label": "Source code", "icon": "github" }
+ ]
+}
+```
+
+Notes on the fields:
+
+- `versions` must be sorted from newest to oldest. `minOpenCloud` is the lowest OpenCloud version your app supports.
+- `badge` is optional and accepts a `label` and a `color` of `primary`, `success` or `danger`.
+- `screenshots` and `resources` are optional.
+
+### Assets
+
+Cover image and screenshots are stored in the awesome-apps repository. Use a 3:2 aspect ratio and prefer PNG or JPEG.
+
+Follow this folder structure:
+
+- `webApps///cover.png`
+- `webApps///screenshots/1.png`
+
+If your repository hosts several apps, add the app name to the path, for example
+`webApps////cover.png`.
+
+Make sure the license of your assets allows us to keep them in the repository and to show them in the app store. If you
+are not sure about the license of an asset, do not add it.
diff --git a/docs/dev/web/extension-system/extension-types/index.md b/docs/dev/web/extension-system/extension-types/index.md
deleted file mode 100644
index a6f04677d..000000000
--- a/docs/dev/web/extension-system/extension-types/index.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: 'Extension Types'
----
-
-This section is a guide about the different predefined extension types of OpenCloud Web. Please refer to the respective
-subpages to learn more about the individual extension types.
diff --git a/docs/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md b/docs/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md
deleted file mode 100644
index 7d7e171e0..000000000
--- a/docs/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md
+++ /dev/null
@@ -1,92 +0,0 @@
----
-title: 'Left sidebar menu item extensions'
-sidebar_position: 5
-id: left-sidebar-menu-item-extensions
----
-
-## Left sidebar menu item extension type
-
-One possible extension type is left sidebar menu items. Registered left sidebar menu items get rendered in the left sidebar, as long as there is more than one available.
-
-### Configuration
-
-To define a left sidebar menu item, you implement the SidebarNavExtension interface.
-It looks like this:
-
-```typescript
-interface SidebarNavExtension {
- id: string
- type: 'sidebarNav'
- extensionPointIds?: string[]
- navItem: AppNavigationItem // Please check the AppNavigationItem section below
- }
-}
-```
-
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
-
-#### AppNavigationItem
-
-The most important configuration options are:
-
-- `icon` - The icon to be displayed, can be picked from [Remix Icon](https://remixicon.com/)
-- `name` - The text to be displayed
-- `route` - The string/route to navigate to, if the nav item should be a `` (Mutually exclusive with `handler`)
-- `handler` - The action to perform upon click, if the nav item should be a `` (Mutually exclusive with `route`)
-
-Please check the [`AppNavigationItem` type](https://github.com/opencloud-eu/web/blob/f069ce44919cde5d112c68a519d433e015a4a011/packages/web-pkg/src/apps/types.ts#L14) for a full list of configuration options.
-
-### Example
-
-The following example shows an extension that adds a left sidebar nav item inside the files app, linking to a custom page. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) package.
-
-```typescript
-export const useCustomPageExtension = () => {
- const { $gettext } = useGettext();
-
- const extension = computed(() => ({
- id: 'com.github.opencloud-eu.web.files.left-nav.custom-page',
- extensionPointIds: ['app.files.navItems'],
- type: 'sidebarNav',
- action: {
- name: $gettext('Custom page'),
- icon: 'world',
- priority: 100,
- isActive: () => true,
- isVisible: () => true,
- route: {
- path: '/files/custom-page'
- },
- activeFor: [{ path: '/files/custom-page' }]
- }
- }));
-
- return { extension };
-};
-```
-
-The extension could then be registered in any app like so:
-
-```typescript
-export default defineWebApplication({
- setup() {
- const { extension } = useCustomPageExtension();
-
- return {
- appInfo: {
- name: $gettext('Custom page app'),
- id: 'custom-page-app'
- },
- routes: {
- path: '/files/custom-page',
- name: 'files-custom-page',
- component: CustomPageComponent,
- meta: {
- title: $gettext('Custom Page')
- }
- },
- extensions: computed(() => [unref(extension)])
- };
- }
-});
-```
diff --git a/docs/dev/web/extension-system/extensions/_category_.json b/docs/dev/web/extension-system/extensions/_category_.json
new file mode 100644
index 000000000..e9387689a
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Extensions",
+ "position": 4
+}
diff --git a/docs/dev/web/extension-system/extensions/extension-points.md b/docs/dev/web/extension-system/extensions/extension-points.md
new file mode 100644
index 000000000..5ea636cc9
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/extension-points.md
@@ -0,0 +1,204 @@
+---
+title: 'Extension Points'
+sidebar_position: 1
+---
+
+Extension points are standardized places where extensions are used. If you build an extension that has the
+[type](./extension-types/) of an extension point, and lists the id of that extension point in its `extensionPointIds`,
+your extension gets used there without any further wiring.
+
+The lists below show the extension points that the OpenCloud Web runtime and the built-in apps provide. The `Multiple`
+column tells you if the extension point renders all matching extensions or only a single one. Your app can also
+[define its own extension points](#defining-your-own-extension-points), so that other apps can extend it.
+
+## Dynamic extension points
+
+Dynamic extension points are specific to each app. `${appId}` is the `id` you define in the `appInfo` of your app,
+for example `files` or `admin-settings`. Use these ids to add something to the user interface of a specific app,
+including apps you do not own (e.g. add a nav item to the sidebar of the files app).
+
+| Extension point id | Extension type | Multiple | Description |
+| ------------------------------------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
+| `app.${appId}.navItems` | `sidebarNav` | yes | Navigation items in the left sidebar. |
+| `app.${appId}.sidebar-nav.main` | `customComponent` | yes | Main area of the left sidebar, below the nav items. |
+| `app.${appId}.sidebar-nav.bottom` | `customComponent` | yes | Bottom area of the left sidebar, above the version info. |
+| `app.${appId}.floating-action-button` | `floatingActionButton` | no | Primary action button. Rendered in the left sidebar on desktop and as a floating action button on mobile. |
+
+## Runtime
+
+| Extension point id | Extension type | Multiple | Description |
+| --------------------------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `app.runtime.header.left` | `customComponent` | yes | Left area of the global top bar. |
+| `app.runtime.header.center` | `customComponent` | yes | Center area of the global top bar. |
+| `app.runtime.header.right` | `customComponent` | yes | Right area of the global top bar. |
+| `app.runtime.header.app-menu` | `appMenuItem` | yes | Application switcher menu in the top left. |
+| `app.runtime.global-progress-bar` | `customComponent` | no | Progress bar for the global loading state. The user can pick one of the registered extensions on the preferences page. |
+| `app.runtime.snackbars` | `customComponent` | yes | Snackbar (toast message) area. |
+| `app.runtime.preferences.panels` | `accountExtension` | yes | Panels on the preferences page, reachable via the top right user menu. |
+
+## Files app
+
+### Actions
+
+| Extension point id | Extension type | Multiple | Description |
+| -------------------------------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------- |
+| `global.files.context-actions` | `action` | yes | Right click context menu of a resource. |
+| `global.files.batch-actions` | `action` | yes | Batch actions in the app bar above file lists. |
+| `global.files.resource-table-actions` | `action` | yes | Inline actions in a row of the resource table. |
+| `global.files.default-action-fallback` | `action` | no | Fallback for the default action (left click) on a resource, used when no other app handles it. |
+| `app.files.sidebar.actions` | `action` | yes | Actions panel of the right sidebar. |
+| `app.files.upload-menu` | `action` | yes | Upload menu. |
+| `app.files.quick-actions` | `action` | yes | Quick actions in a row of the resource table. |
+| `app.files.trash-quick-actions` | `action` | yes | Quick actions in a row of the trash overview. |
+
+### Right sidebar
+
+| Extension point id | Extension type | Multiple | Description |
+| --------------------------------------------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
+| `global.files.sidebar` | `sidebarPanel` | yes | Panels of the right sidebar. Used in any file context, meaning the files app as well as viewer and editor apps. |
+| `app.files.sidebar.file-details.table` | `customComponent` | no | Details table of a file. `space` and `resource` can be retrieved via injection context. |
+| `app.files.sidebar.space-details.table` | `customComponent` | no | Details table of a space. `space` and `resource` can be retrieved via injection context. |
+| `app.files.sidebar.shares-panel.shared-with.top` | `customComponent` | no | Top section of the people list in the shares panel. `space` and `resource` can be retrieved via injection context. |
+| `app.files.sidebar.shares-panel.shared-with.bottom` | `customComponent` | no | Bottom section of the people list in the shares panel. `space` and `resource` can be retrieved via injection context. |
+
+### Folder views
+
+Folder views define how the content of a page is presented, for example as a table or as a tile grid. Each page has its
+own extension point.
+
+| Extension point id | Extension type | Multiple | Description |
+| ------------------------------------------- | -------------- | -------- | ------------------------ |
+| `app.files.folder-views.folder` | `folderView` | no | Regular folders. |
+| `app.files.folder-views.project-spaces` | `folderView` | no | Project spaces overview. |
+| `app.files.folder-views.favorites` | `folderView` | no | Favorites page. |
+| `app.files.folder-views.trash` | `folderView` | no | Trash of a single space. |
+| `app.files.folder-views.trash-overview` | `folderView` | no | Trash overview. |
+| `app.files.folder-views.shared-with-me` | `folderView` | no | Shared with me page. |
+| `app.files.folder-views.shared-with-others` | `folderView` | no | Shared with others page. |
+| `app.files.folder-views.shared-via-link` | `folderView` | no | Shared via link page. |
+| `app.files.folder-views.search` | `folderView` | no | Search results page. |
+
+### Other
+
+| Extension point id | Extension type | Multiple | Description |
+| ---------------------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------ |
+| `app.files.floating-action-button` | `floatingActionButton` | no | Primary action button of the files app. Instance of the dynamic extension point described above. |
+| `global.files.resource-indicator` | `resourceIndicator` | yes | Status icons and tags shown next to the name of a resource. |
+
+## Other apps
+
+| Extension point id | Extension type | Multiple | Description |
+| ----------------------------- | -------------- | -------- | ---------------------------------------------------------- |
+| `app.search.provider` | `search` | yes | Search engines for the search input in the global top bar. |
+| `app.preview.toolbar-actions` | `action` | yes | Toolbar of the preview app. |
+
+## Defining your own extension points
+
+Define an extension point wherever your app has a place that other apps may fill. A good example is a toolbar or a
+panel that is useful beyond your own use case.
+
+### Declaring an extension point
+
+An extension point is a plain object of the `ExtensionPoint` type. Keep it in its own file, for example
+`src/extensionPoints.ts`, so that you can use it in several places:
+
+```typescript title="src/extensionPoints.ts"
+import { ActionExtension, ExtensionPoint } from '@opencloud-eu/web-pkg';
+
+export const toolbarExtensionPoint: ExtensionPoint = {
+ id: 'app.my-app.toolbar',
+ extensionType: 'action',
+ multiple: true
+};
+```
+
+`ExtensionPoint` accepts the following keys:
+
+- `id` - the id of the extension point. Extensions list it in their `extensionPointIds`. Use the same dot-formatted
+ namespace as for extension ids, and put your app id in it.
+- `extensionType` - the [type](./extension-types/) of the extensions that this extension point accepts.
+- `multiple` - whether all matching extensions are used, or only a single one. Defaults to `false`.
+- `defaultExtensionId` - the extension that is used when `multiple` is `false` and the user did not pick one.
+- `userPreference` - makes the extension point configurable by users, see below.
+
+### Registering an extension point
+
+Return your extension points from the app definition. The key takes a `Ref`, so use a `computed`:
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+import { toolbarExtensionPoint } from './extensionPoints';
+
+export default defineWebApplication({
+ setup() {
+ return {
+ appInfo: {
+ name: 'My app',
+ id: 'my-app'
+ },
+ extensionPoints: computed(() => [toolbarExtensionPoint])
+ };
+ }
+});
+```
+
+Registration is not needed to query extensions. It makes your extension point known to the runtime, which is required
+for the preferences page, and it documents the extension point for other developers.
+
+### Rendering the extensions
+
+For the type `customComponent`, mount a `CustomComponentTarget` at the place where the extensions belong. It queries
+the registry and respects the user preference:
+
+```html title="src/App.vue"
+
+
+
+
+
+```
+
+For all other types, query the registry yourself and render the result the way your extension point needs it:
+
+```typescript
+import { ActionExtension, useExtensionRegistry } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+import { toolbarExtensionPoint } from './extensionPoints';
+
+const extensionRegistry = useExtensionRegistry();
+
+const actions = computed(() =>
+ extensionRegistry
+ .requestExtensions(toolbarExtensionPoint)
+ .map(({ action }) => action)
+);
+```
+
+### Letting users choose
+
+Add a `userPreference` to your extension point to let users pick one of the registered extensions. This only makes
+sense when `multiple` is `false`:
+
+```typescript title="src/extensionPoints.ts"
+import { ExtensionPoint, CustomComponentExtension } from '@opencloud-eu/web-pkg';
+import { useGettext } from 'vue3-gettext';
+
+const { $gettext } = useGettext();
+
+export const progressBarExtensionPoint: ExtensionPoint = {
+ id: 'app.my-app.progress-bar',
+ extensionType: 'customComponent',
+ userPreference: {
+ label: $gettext('Progress bar'),
+ description: $gettext('Choose how the loading state is shown.')
+ }
+};
+```
+
+The extension point then gets a dropdown on the preferences page, reachable via the top right user menu. The dropdown
+only appears when at least one extension is registered for the extension point. Each extension can provide its own
+`userPreference.optionLabel` as the label in that dropdown.
diff --git a/docs/dev/web/extension-system/extension-types/_category_.json b/docs/dev/web/extension-system/extensions/extension-types/_category_.json
similarity index 100%
rename from docs/dev/web/extension-system/extension-types/_category_.json
rename to docs/dev/web/extension-system/extensions/extension-types/_category_.json
diff --git a/docs/dev/web/extension-system/extensions/extension-types/account-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/account-extensions.md
new file mode 100644
index 000000000..a25f38c4f
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/extension-types/account-extensions.md
@@ -0,0 +1,46 @@
+---
+title: 'Account extensions'
+sidebar_position: 9
+id: account-extensions
+---
+
+## Extension Type AccountExtension
+
+Account extensions add a panel to the preferences page, which is reachable via the user menu in the top right. Each
+registered extension gets its own entry in the navigation of that page.
+
+### Configuration
+
+To define an account extension, you implement the `AccountExtension` interface. Here's what it looks like:
+
+```typescript
+interface AccountExtension {
+ id: string;
+ type: 'accountExtension';
+ extensionPointIds?: string[];
+ content: Slot | Component;
+ label: () => string;
+ icon: string;
+}
+```
+
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
+
+- `content` - The component to render inside the panel.
+- `label` - Returns the title of the panel. Wrap it in `$gettext` to make it translatable.
+- `icon` - The icon of the navigation entry, can be picked from [Remix Icon](https://remixicon.com/).
+
+### Example
+
+The following example is taken from the Web runtime. It adds the app tokens panel to the preferences page.
+
+```typescript title="src/extensions.ts"
+const extension: AccountExtension = {
+ id: 'com.github.opencloud-eu.web.runtime.preferences-panels.app-tokens',
+ type: 'accountExtension',
+ extensionPointIds: ['app.runtime.preferences.panels'],
+ label: () => $gettext('App Tokens'),
+ icon: 'key-2',
+ content: AppTokens
+};
+```
diff --git a/docs/dev/web/extension-system/extension-types/action-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/action-extensions.md
similarity index 92%
rename from docs/dev/web/extension-system/extension-types/action-extensions.md
rename to docs/dev/web/extension-system/extensions/extension-types/action-extensions.md
index d726150fc..6d1509ff7 100644
--- a/docs/dev/web/extension-system/extension-types/action-extensions.md
+++ b/docs/dev/web/extension-system/extensions/extension-types/action-extensions.md
@@ -21,7 +21,7 @@ interface ActionExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
#### Action
@@ -41,7 +41,7 @@ Please check the [`Action` type](https://github.com/opencloud-eu/web/blob/236c18
The following example shows how an action extension for downloading files could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/master/packages/web-pkg) package.
-```typescript
+```typescript title="src/extensions.ts"
export const useDownloadFilesExtension = () => {
const { $gettext } = useGettext();
@@ -73,7 +73,7 @@ export const useDownloadFilesExtension = () => {
The extension could then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useFileActionDownloadFiles();
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/app-menu-item-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/app-menu-item-extensions.md
similarity index 91%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/app-menu-item-extensions.md
rename to docs/dev/web/extension-system/extensions/extension-types/app-menu-item-extensions.md
index fa2c6f2fd..4f06e356d 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/app-menu-item-extensions.md
+++ b/docs/dev/web/extension-system/extensions/extension-types/app-menu-item-extensions.md
@@ -19,7 +19,7 @@ interface AppMenuItemExtension {
extensionPointIds?: string[];
label: () => string;
color?: string;
- handler?: () => void;
+ handler?: () => Promise | void;
icon?: string;
path?: string;
priority?: number;
@@ -27,7 +27,7 @@ interface AppMenuItemExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
A `handler` will result in a `` element. This is necessary when an action should be performed when clicking the menu item (e.g. opening a file editor).
@@ -43,7 +43,7 @@ At least one of these properties has to be provided when registering an extensio
The following example shows how an app creates an extension that registers an app menu item, linking to an internal page. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) package.
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { $gettext } = useGettext();
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/custom-component-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/custom-component-extensions.md
similarity index 80%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/custom-component-extensions.md
rename to docs/dev/web/extension-system/extensions/extension-types/custom-component-extensions.md
index d55be79a7..5ff4af3ee 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/custom-component-extensions.md
+++ b/docs/dev/web/extension-system/extensions/extension-types/custom-component-extensions.md
@@ -20,19 +20,20 @@ interface CustomComponentExtension {
type: 'customComponent';
extensionPointIds?: string[];
content: Slot | Component;
+ componentProps?: () => Record;
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
-The `content` property specifies a render function or a Component for the target extension point.
+The `content` property specifies a render function or a Component for the target extension point. Use `componentProps` to pass props to that component.
### Example
A simple example for a custom component extension could be a `NyanCat` progress bar component, being
targeted at the `global-progress-bar` extension point as render target.
-```typescript
+```typescript title="src/extensions.ts"
const extension = {
id: 'com.github.opencloud-eu.web.app.progress-bars.nyan-cat',
type: 'customComponent',
diff --git a/docs/dev/web/extension-system/extension-types/floating-action-button-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/floating-action-button-extensions.md
similarity index 76%
rename from docs/dev/web/extension-system/extension-types/floating-action-button-extensions.md
rename to docs/dev/web/extension-system/extensions/extension-types/floating-action-button-extensions.md
index 8b6772897..8cf972396 100644
--- a/docs/dev/web/extension-system/extension-types/floating-action-button-extensions.md
+++ b/docs/dev/web/extension-system/extensions/extension-types/floating-action-button-extensions.md
@@ -9,7 +9,7 @@ id: floating-action-button-extensions
This extension type allows apps to register actions either displayed within the left sidebar (for desktop resolutions) or as a floating action button (for mobile resolutions). The extension point for this extension type is `app.${appId}.floating-action-button`.
:::warning
-You need to take care of the visibility of your floating action button extension via the `isActive` property, otherwise you might end up overwriting other extensions' action buttons. In most cases, it makes sense to only display the button when your app is currently active.
+You need to take care of the visibility of your floating action button extension via the `isVisible` property, otherwise you might end up overwriting other extensions' action buttons. In most cases, it makes sense to only display the button when your app is currently active.
:::
### Configuration
@@ -22,7 +22,8 @@ interface FloatingActionButtonExtension {
type: 'floatingActionButton';
extensionPointIds?: string[];
label: () => string;
- isActive: () => boolean;
+ tooltip?: () => string;
+ isVisible?: () => boolean;
isDisabled?: () => boolean;
color?: string;
icon?: string;
@@ -32,11 +33,11 @@ interface FloatingActionButtonExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
The `mode` property determines whether the floating action button triggers a handler function or renders a `dropComponent` when clicked. If mode returns `handler`, the click handler function is executed, if it returns `drop`, the specified `dropComponent` is shown instead.
-`isDisabled` controls the disabled state of the button whereas `isActive` determines if the button is showing at all.
+`isDisabled` controls the disabled state of the button whereas `isVisible` determines if the button is showing at all. `tooltip` returns the text of the tooltip, for example to explain why the button is disabled.
`icon` is an icon name string that can be picked from [Remix Icon](https://remixicon.com/).
@@ -44,9 +45,10 @@ The `mode` property determines whether the floating action button triggers a han
The following example shows how the files app is registering a floating action button extension for creating new files or folders. Note that the example assumes you're in a Vue injection context (e.g. within the `setup` method of your app's `defineWebApplication` call).
-```typescript
+```typescript title="src/extensions.ts"
import { useGettext } from 'vue3-gettext';
import CreateOrUploadMenu from './components/CreateOrUploadMenu.vue';
+import { markRaw } from 'vue';
import { useIsFilesAppActive, useResourcesStore } from '@opencloud-eu/web-pkg';
const { $gettext } = useGettext();
@@ -59,12 +61,13 @@ const extension = {
type: 'floatingActionButton',
icon: 'add',
label: () => $gettext('New'),
- isActive: () => {
+ isVisible: () => {
return unref(isFilesAppActive);
},
isDisabled: () => {
return !resourcesStore.currentFolder?.canUpload();
},
- dropComponent: CreateOrUploadMenu
+ mode: () => 'drop',
+ dropComponent: markRaw(CreateOrUploadMenu)
};
```
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/folder-view-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/folder-view-extensions.md
similarity index 85%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/folder-view-extensions.md
rename to docs/dev/web/extension-system/extensions/extension-types/folder-view-extensions.md
index f18d81654..4795c81cd 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/folder-view-extensions.md
+++ b/docs/dev/web/extension-system/extensions/extension-types/folder-view-extensions.md
@@ -21,7 +21,7 @@ interface FolderViewExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
#### FolderView
@@ -30,15 +30,14 @@ For the folderView object, you have the following configuration options:
- `name` - The name of the action (not displayed in the UI)
- `label` - The text to be displayed to the user when switching between different FolderView options
- `icon` - Object, expecting an icon `name` and a corresponding `IconFillType`, see [Remix Icon](https://remixicon.com/) for available options
-- `isScrollable` - Optional boolean, determines whether the user can scroll inside the component or it statically fills the viewport
- `component` - The Vue component to render the resources. It should expect a prop of type `Resource[]`
-- `componentAttrs` - Optional additional configuration for the component mentioned above
+- `componentAttrs` - Optional function returning additional props for the component mentioned above
### Example
The following example shows how an extension for a custom folder view could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) package.
-```typescript
+```typescript title="src/extensions.ts"
export const useCustomFolderViewExtension = () => {
const { $gettext } = useGettext();
@@ -63,7 +62,7 @@ export const useCustomFolderViewExtension = () => {
The extension could then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useCustomFolderViewExtension();
diff --git a/docs/dev/web/extension-system/extensions/extension-types/index.md b/docs/dev/web/extension-system/extensions/extension-types/index.md
new file mode 100644
index 000000000..3a5ec591c
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/extension-types/index.md
@@ -0,0 +1,25 @@
+---
+title: 'Extension Types'
+---
+
+For building an extension you can choose from the types predefined by the OpenCloud Web extension system. The full list
+is shown below. Please refer to the respective subpages to learn more about the individual extension types.
+
+| Extension type | `type` | Description |
+| ---------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [`ActionExtension`](./action-extensions) | `action` | Registers `Action` items that get shown in various places, for example context menus or batch actions. Most commonly used for file and folder actions like copy, rename or delete. |
+| [`AppMenuItemExtension`](./app-menu-item-extensions) | `appMenuItem` | Registers links to internal or external pages in the application switcher menu. |
+| [`CustomComponentExtension`](./custom-component-extensions) | `customComponent` | Registers a custom component for a render target. |
+| [`FolderViewExtension`](./folder-view-extensions) | `folderView` | Registers additional ways of displaying the content of a folder, meaning resources like spaces, folders or files. |
+| [`SidebarNavExtension`](./left-sidebar-menu-item-extensions) | `sidebarNav` | Registers additional navigation items for the left sidebar. These can be scoped to specific apps, and enabled or disabled programmatically. |
+| [`SidebarPanelExtension`](./right-sidebar-panel-extensions) | `sidebarPanel` | Registers panels for the right sidebar. |
+| [`SearchExtension`](./search-extensions) | `search` | Registers additional search providers. |
+| [`FloatingActionButtonExtension`](./floating-action-button-extensions) | `floatingActionButton` | Registers one or multiple primary actions. Displayed in the left sidebar on desktop resolutions and as a floating action button on mobile resolutions. |
+| [`AccountExtension`](./account-extensions) | `accountExtension` | Registers a panel on the preferences page. |
+| [`ResourceIndicatorExtension`](./resource-indicator-extensions) | `resourceIndicator` | Registers status icons or tags that get shown next to the name of a resource. |
+| [`VaultExtension`](./vault-extensions) | `vault` | Registers a client side encryption scheme for vault folders and vault spaces. |
+
+You're free to introduce your own extension types within your application code and use the extension registry to query
+the available ones. However, if you have the impression that an important extension type is missing and would be
+beneficial for the platform, please reach out to us by opening a
+[GitHub issue](https://github.com/opencloud-eu/web/issues/new/choose).
diff --git a/docs/dev/web/extension-system/extensions/extension-types/left-sidebar-menu-item-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/left-sidebar-menu-item-extensions.md
new file mode 100644
index 000000000..b21645c7f
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/extension-types/left-sidebar-menu-item-extensions.md
@@ -0,0 +1,93 @@
+---
+title: 'Left sidebar menu item extensions'
+sidebar_position: 5
+id: left-sidebar-menu-item-extensions
+---
+
+## Left sidebar menu item extension type
+
+One possible extension type is left sidebar menu items. Registered left sidebar menu items get rendered in the left sidebar, as long as there is more than one available.
+
+### Configuration
+
+To define a left sidebar menu item, you implement the `SidebarNavExtension` interface. It looks like this:
+
+```typescript
+interface SidebarNavExtension {
+ id: string;
+ type: 'sidebarNav';
+ extensionPointIds?: string[];
+ navItem: AppNavigationItem; // Please check the AppNavigationItem section below
+}
+```
+
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
+
+Each app has its own nav items extension point with the id `app.${appId}.navItems`. Use it in `extensionPointIds` to
+place your nav item in the left sidebar of that app.
+
+#### AppNavigationItem
+
+The most important configuration options are:
+
+- `icon` - The icon to be displayed, can be picked from [Remix Icon](https://remixicon.com/)
+- `name` - The text to be displayed
+- `route` - The string/route to navigate to, if the nav item should be a `` (Mutually exclusive with `handler`)
+- `handler` - The action to perform upon click, if the nav item should be a `` (Mutually exclusive with `route`)
+
+Please check the [`AppNavigationItem` type](https://github.com/opencloud-eu/web/blob/main/packages/web-pkg/src/apps/types.ts) for a full list of configuration options.
+
+### Example
+
+The following example is taken from the [office settings app](https://github.com/opencloud-eu/web/blob/main/packages/web-app-office-settings/src/index.ts). It adds a nav item to the left sidebar of the admin settings app, linking to a view of the office settings app.
+
+```typescript title="src/index.ts"
+import { defineWebApplication, SidebarNavExtension } from '@opencloud-eu/web-pkg';
+import { useGettext } from 'vue3-gettext';
+import { computed } from 'vue';
+import App from './App.vue';
+
+export default defineWebApplication({
+ setup() {
+ const { $gettext } = useGettext();
+ const appId = 'admin-settings/office';
+
+ const extensions = computed(() => [
+ {
+ id: 'com.github.opencloud-eu.web.admin-settings.left-nav.office',
+ type: 'sidebarNav',
+ extensionPointIds: ['app.admin-settings.navItems'],
+ navItem: {
+ isVisible: () => true,
+ name: $gettext('Office'),
+ icon: 'attachment',
+ route: {
+ path: `/${appId}`
+ }
+ }
+ }
+ ]);
+
+ const routes = [
+ {
+ path: '/',
+ name: 'office',
+ component: App,
+ meta: {
+ title: $gettext('Office Settings'),
+ authContext: 'user'
+ }
+ }
+ ];
+
+ return {
+ appInfo: {
+ name: $gettext('Office Settings'),
+ id: appId
+ },
+ routes,
+ extensions
+ };
+ }
+});
+```
diff --git a/docs/dev/web/extension-system/extensions/extension-types/resource-indicator-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/resource-indicator-extensions.md
new file mode 100644
index 000000000..85e5da0a6
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/extension-types/resource-indicator-extensions.md
@@ -0,0 +1,85 @@
+---
+title: 'Resource indicator extensions'
+sidebar_position: 10
+id: resource-indicator-extensions
+---
+
+## Extension Type ResourceIndicator
+
+Resource indicator extensions add status icons or tags next to the name of a resource, for example in the resource
+table. They are useful to show extra state of a file, folder or space, such as a lock or a label.
+
+### Configuration
+
+To define a resource indicator extension, you implement the `ResourceIndicatorExtension` interface. Here's what it looks
+like:
+
+```typescript
+interface ResourceIndicatorExtension {
+ id: string;
+ type: 'resourceIndicator';
+ extensionPointIds?: string[];
+ getResourceIndicators: (resource: Resource) => ResourceIndicator[] | void;
+}
+```
+
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
+
+`getResourceIndicators` gets called for every rendered resource. Return an empty value if your extension has nothing to
+show for the given resource. This function runs very often, so keep it cheap and return early where possible.
+
+An indicator is either an icon or a tag:
+
+```typescript
+interface ResourceIndicatorIcon {
+ id: string;
+ kind: 'icon';
+ label: string;
+ accessibleDescription: string;
+ type: string;
+ category: 'system' | 'sharing' | 'space';
+ icon: string;
+ fillType: IconFillType;
+ handler?: (resource: Resource, event?: MouseEvent) => void;
+}
+
+interface ResourceIndicatorTag {
+ id: string;
+ kind: 'tag';
+ label: string;
+ accessibleDescription: string;
+ type: string;
+ category: 'system' | 'sharing' | 'space';
+ class?: string;
+}
+```
+
+### Example
+
+The following example adds a lock icon to every resource that is marked as read only.
+
+```typescript title="src/extensions.ts"
+const extension: ResourceIndicatorExtension = {
+ id: 'com.github.opencloud-eu.web.app.resource-indicator.read-only',
+ type: 'resourceIndicator',
+ extensionPointIds: ['global.files.resource-indicator'],
+ getResourceIndicators(resource) {
+ if (resource.canRename()) {
+ return;
+ }
+
+ return [
+ {
+ id: `read-only-${resource.id}`,
+ kind: 'icon',
+ label: $gettext('Read only'),
+ accessibleDescription: $gettext('This item cannot be modified'),
+ icon: 'lock-2',
+ fillType: 'line',
+ category: 'system',
+ type: 'read-only'
+ }
+ ];
+ }
+};
+```
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/right-sidebar-panel-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/right-sidebar-panel-extensions.md
similarity index 95%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/right-sidebar-panel-extensions.md
rename to docs/dev/web/extension-system/extensions/extension-types/right-sidebar-panel-extensions.md
index 91baa35fb..1351c586b 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/right-sidebar-panel-extensions.md
+++ b/docs/dev/web/extension-system/extensions/extension-types/right-sidebar-panel-extensions.md
@@ -28,7 +28,7 @@ interface SidebarPanelExtension
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
The `panel` object configures the actual sidebar panel. It consists of different properties and functions, where all the functions get called with a
`SideBarPanelContext` entity from the integrating extension points.
@@ -83,7 +83,7 @@ will automatically become available in all environments that display the `FileSi
The following example shows how a sidebar panel for displaying exif data for a resource could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) and the [web-client](https://github.com/opencloud-eu/web/tree/main/packages/web-client) packages.
-```typescript
+```typescript title="src/extensions.ts"
export const useExifDataPanelExtension = () => {
const { $gettext } = useGettext();
@@ -113,7 +113,7 @@ export const useExifDataPanelExtension = () => {
The extension can then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useExifDataPanelExtension();
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/search-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/search-extensions.md
similarity index 93%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/search-extensions.md
rename to docs/dev/web/extension-system/extensions/extension-types/search-extensions.md
index aa59eb46b..1971be525 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/search-extensions.md
+++ b/docs/dev/web/extension-system/extensions/extension-types/search-extensions.md
@@ -28,7 +28,7 @@ interface SearchExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
The `searchProvider` object configures the actual provider. It consist of the following:
@@ -55,7 +55,7 @@ The previewSearch object extends the listSearch with one additional attribute:
The following example shows how a search extension that queries a Solr search engine could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) and the [web-client](https://github.com/opencloud-eu/web/tree/main/packages/web-client) packages.
-```typescript
+```typescript title="src/extensions.ts"
export const useSolrSearchExtension = () => {
const { $gettext } = useGettext();
@@ -87,7 +87,7 @@ export const useSolrSearchExtension = () => {
The search component for the preview search container may look like this:
-```html
+```html title="src/components/SolarSearchComponent.vue"
@@ -105,7 +105,7 @@ The search component for the preview search container may look like this:
The extension can then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useSolrSearchExtension();
diff --git a/docs/dev/web/extension-system/extensions/extension-types/vault-extensions.md b/docs/dev/web/extension-system/extensions/extension-types/vault-extensions.md
new file mode 100644
index 000000000..436a3a404
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/extension-types/vault-extensions.md
@@ -0,0 +1,123 @@
+---
+title: 'Vault extensions'
+sidebar_position: 11
+id: vault-extensions
+---
+
+## Extension Type Vault
+
+Vault extensions add a client side encryption scheme to OpenCloud Web. A vault is a folder or a space whose content, and
+usually also whose resource names, are encrypted in the browser before they reach the server.
+
+The Web runtime stays scheme-agnostic. It only knows that some location is a vault and whether it is unlocked. All
+cryptography, all key handling and the unlock user interface come from the extension.
+
+:::note
+This extension type is meant for encryption schemes. If you only want to add functionality to a vault, use one of the
+other extension types instead. A reference implementation is the
+[rclone-crypt app](https://github.com/opencloud-eu/web/tree/main/packages/web-app-rclone-crypt).
+:::
+
+### Configuration
+
+To define a vault extension, you implement the `VaultExtension` interface. Here's what it looks like:
+
+```typescript
+interface VaultExtension {
+ id: string;
+ type: 'vault';
+ extensionPointIds?: string[];
+ claimsPath: (space: SpaceResource, path: string) => VaultClaim | null;
+ resolve: (space: SpaceResource, path: string) => Promise;
+ creation?: VaultCreation;
+}
+```
+
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
+
+- `claimsPath` - Tells the runtime if your extension is responsible for the given location, no matter if the vault is
+ unlocked. This is a cheap, synchronous check. Return `null` if the location is not one of your vaults.
+- `resolve` - Returns the engine that encrypts and decrypts for the given location. Return `null` if your extension is
+ not responsible, or if the vault is locked.
+- `creation` - Optional. Its presence tells the user interface that your extension can create new vaults, so an
+ encryption option is offered when a user creates a folder or a space.
+
+#### VaultClaim
+
+```typescript
+interface VaultClaim {
+ vaultRoot: string;
+ encryptsNames: boolean;
+ unlockRoute?: RouteLocationNamedRaw;
+}
+```
+
+- `vaultRoot` - The clear text root of the vault, for example `/my-vault.vault`. Use `/` for a vault space.
+- `encryptsNames` - Set this to `true` if your scheme also encrypts resource names, not just their content.
+- `unlockRoute` - The route that asks the user to unlock the vault. Your route handler fills the vault store and then
+ redirects back to `query.redirectUrl`. Without this route the vault is treated as permanently locked.
+
+#### VaultEngine
+
+The engine does the actual cryptography. All of its path methods work on paths that are relative to the vault root. A
+bare resource name is a relative path with one segment, so a name must encrypt independently of its position in the tree.
+
+```typescript
+interface VaultEngine {
+ vaultRoot: string;
+ encryptPath: (relativePath: string) => Promise;
+ decryptPath: (relativePath: string) => Promise;
+ encryptContent: (plaintext: ReadableStream) => ReadableStream;
+ decryptContent: (encrypted: ReadableStream) => ReadableStream;
+ createIntegrityToken: () => Promise;
+ verifyIntegrityToken: (token: string) => Promise;
+ verifySegment: (sampleEncryptedSegment: string) => Promise;
+}
+```
+
+The integrity token commits a vault to the key of your engine. It gets written once, when the secret of a vault is first
+set, and it is stored as a WebDAV property on the vault root. Its format is up to your engine. `verifySegment` is the
+weaker fallback for vaults that carry no token, for example vaults created outside of OpenCloud Web.
+
+If you have full clear text paths, use the `encryptVaultPath` and `decryptVaultPath` helpers from `web-pkg` instead of
+calling the engine directly.
+
+#### VaultCreation
+
+```typescript
+interface VaultCreation {
+ vaultExtension: string;
+ vaultContentType: string;
+ setupComponent: Component;
+}
+```
+
+- `vaultExtension` - The name extension a vault folder carries, without the leading dot, for example `vault`.
+- `vaultContentType` - The content type a vault space carries in its `@libre.graph.contentType` drive property.
+- `setupComponent` - The component that collects and commits the secret of a new vault. It is rendered as the second
+ step of the create folder or create space flow. It takes a `vaultName` prop, emits `update:valid`, and exposes a
+ `finalize` function that gets called once the folder or space exists on the server.
+
+### Example
+
+```typescript title="src/extensions.ts"
+import { markRaw } from 'vue';
+import { VaultExtension } from '@opencloud-eu/web-pkg';
+import VaultSetup from './components/VaultSetup.vue';
+
+export const vaultSchemeExtension: VaultExtension = {
+ id: 'app.rclone-crypt.vault',
+ type: 'vault',
+ resolve(space, path) {
+ return Promise.resolve(resolveVault(space, path));
+ },
+ claimsPath(space, path) {
+ return claimsVaultPath(space, path);
+ },
+ creation: {
+ vaultExtension: 'vault',
+ vaultContentType: 'application/vnd.opencloud.vault',
+ setupComponent: markRaw(VaultSetup)
+ }
+};
+```
diff --git a/docs/dev/web/extension-system/extensions/index.md b/docs/dev/web/extension-system/extensions/index.md
new file mode 100644
index 000000000..d8bff39f5
--- /dev/null
+++ b/docs/dev/web/extension-system/extensions/index.md
@@ -0,0 +1,133 @@
+---
+title: 'Extensions'
+---
+
+Extensions are small, scoped pieces of functionality that can be added to the OpenCloud Web interface. They typically
+hook into specific [extension points](./extension-points) and always need to be shipped by an app.
+
+Every extension has a [type](./extension-types/), which defines what the extension does and which data it provides.
+The extension point it registers on defines where it appears in OpenCloud Web.
+
+## Extension registry
+
+The OpenCloud Web runtime provides a globally available extension registry. It is used to both register and query
+extensions. All extensions that are made available via an app get registered in the extension registry automatically.
+
+## Base configuration
+
+Any extension is required to define at least an `id` and a `type` in order to fulfill the generic `Extension` interface.
+
+The `id` is supposed to be unique throughout the OpenCloud Web ecosystem. In order to keep `id`s readable for humans we
+didn't want to enforce uniqueness through e.g. uuids. Instead, we chose to use dot-formatted namespaces like e.g.
+`com.github.opencloud-eu.web.files.search`. We'd like to encourage you to follow the same format for your own extensions.
+
+For the `type` you can choose from the [predefined extension types](./extension-types/) or define a custom one.
+
+In addition, you can pass optional `extensionPointIds` to determine where the extension will appear. You can find all
+predefined ids in the [extension points docs](./extension-points).
+
+## Registering extensions
+
+### Via the app definition
+
+The `extensions` key of an app definition takes a `Ref`, so use a `computed`. The runtime registers its
+content when your app is loaded:
+
+```typescript title="src/index.ts"
+import { defineWebApplication, Extension } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+
+export default defineWebApplication({
+ setup() {
+ const extensions = computed(() => [
+ {
+ id: 'com.github.my-org.my-app.my-action',
+ type: 'action',
+ extensionPointIds: ['global.files.context-actions'],
+ action: {
+ // See the action extension docs
+ }
+ }
+ ]);
+
+ return {
+ appInfo: {
+ name: 'My app',
+ id: 'my-app'
+ },
+ extensions
+ };
+ }
+});
+```
+
+Because the value is a ref, the list is reactive. You can hide or show an extension based on the app config, the
+capabilities of the server, or the permissions of the user:
+
+```typescript
+const extensions = computed(() => {
+ if (!unref(isFeatureAvailable)) {
+ return [];
+ }
+ return [myExtension];
+});
+```
+
+### At runtime
+
+You can also register extensions from any place that has access to the injection context, for example a component or a
+composable:
+
+```typescript
+import { useExtensionRegistry } from '@opencloud-eu/web-pkg';
+
+const extensionRegistry = useExtensionRegistry();
+
+extensionRegistry.registerExtensions(computed(() => [myExtension]));
+```
+
+Remove them again via `unregisterExtensions`, which takes the ids:
+
+```typescript
+extensionRegistry.unregisterExtensions([myExtension.id]);
+```
+
+Prefer the app definition. Use the runtime API only when the extensions are not known at that point, for example because
+they depend on data that you load first.
+
+:::note
+Administrators can switch off single extensions via the `options.disabledExtensions` key of the OpenCloud Web config. A
+disabled extension stays registered, but the registry does not return it.
+:::
+
+## Querying extensions
+
+Use `requestExtensions` to get all extensions of an extension point. The registry returns only the extensions whose
+`type` matches the `extensionType` of the extension point, and whose `extensionPointIds` allow this extension point:
+
+```typescript
+import { ActionExtension, useExtensionRegistry } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+
+const extensionRegistry = useExtensionRegistry();
+
+const actions = computed(() =>
+ extensionRegistry.requestExtensions(myExtensionPoint).map(({ action }) => action)
+);
+```
+
+To learn how to define `myExtensionPoint`, please refer to the
+[extension points docs](./extension-points#defining-your-own-extension-points).
+
+## User preferences
+
+Extension points can let users choose between the registered extensions. The extension point then gets a dropdown on
+the preferences page, reachable via the top right user menu. Your extension provides the label for that dropdown via
+`userPreference.optionLabel`. For the extension point side, please refer to the
+[extension points docs](./extension-points#letting-users-choose).
+
+## Creating an extension
+
+Please check out the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) for a boilerplate
+app that also includes an extension. In addition to that, the [extension types docs](./extension-types/) provide
+instructions and examples on how to implement the different extension types.
diff --git a/docs/dev/web/extension-system/getting-started.md b/docs/dev/web/extension-system/getting-started.md
new file mode 100644
index 000000000..595abec01
--- /dev/null
+++ b/docs/dev/web/extension-system/getting-started.md
@@ -0,0 +1,172 @@
+---
+title: 'Getting started'
+sidebar_position: 1
+---
+
+This guide shows how to set up and run an OpenCloud Web app.
+
+## Prerequisites
+
+- git
+- docker and docker compose
+- node
+- [pnpm](https://pnpm.io/installation), ideally installed via `corepack`
+
+If you don't use Docker Desktop, add `127.0.0.1 host.docker.internal` to your `/etc/hosts` file. Otherwise
+`host.docker.internal` cannot be resolved.
+
+## Project setup
+
+The fastest way to start is the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton). It
+contains a working app, a docker compose setup with an OpenCloud server, and a unit test setup.
+
+```bash
+git clone https://github.com/opencloud-eu/web-app-skeleton.git my-app
+cd my-app
+pnpm install
+```
+
+Rename the app afterwards. The name `skeleton` appears in `package.json`, `vite.config.ts`, `src/index.ts`,
+and `tests/unit/App.spec.ts`.
+
+## Running your app
+
+There are two ways to run your app against a local OpenCloud instance.
+
+### Watch build
+
+This mode fully builds your app and writes it into the `dist` folder, which is then served by the OpenCloud server.
+
+1. Start a watch build. It writes your app into the `dist` folder on every change.
+
+ ```bash
+ pnpm build:w
+ ```
+
+2. Start the OpenCloud server. In the skeleton repository, the `dist` folder is already mounted into the container, and
+ `WEB_ASSET_APPS_PATH` points to the mount target.
+
+ ```bash
+ docker compose up
+ ```
+
+3. Open [https://host.docker.internal:9200](https://host.docker.internal:9200) and log in as `admin` with the password
+ `admin`. Your app is loaded automatically.
+
+Changes are picked up by the watch build, but you need to reload the page to see them.
+
+### Module federation with hot reload
+
+In this mode your app is served by its own Vite dev server and loaded into a running OpenCloud Web dev server as a
+federated module. You get instant hot reload, but you need a local checkout of the
+[web repository](https://github.com/opencloud-eu/web).
+
+1. Start the OpenCloud Web dev server via `pnpm vite` in your `web` checkout, as described in the
+ [tooling docs](../development/tooling#using-instant-hot-reload-via-vite). It listens on
+ [https://host.docker.internal:9201](https://host.docker.internal:9201).
+
+2. Start the dev server of your app:
+
+ ```bash
+ pnpm vite
+ ```
+
+ It listens on port `9210` by default. Change it via the `server.port` option in your Vite config.
+
+3. Open [https://host.docker.internal:9210](https://host.docker.internal:9210) and accept the self-signed certificate
+ (adjust the port if you changed it in your Vite config).
+
+4. Open [https://host.docker.internal:9201](https://host.docker.internal:9201).
+
+The extension-sdk registers your app with the OpenCloud Web dev server every few seconds, so the registration survives a
+restart of either server.
+
+## The app definition
+
+The `src/index.ts` file acts as the entrypoint of the app. This file has to export an app definition created via
+`defineWebApplication`:
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg'
+import { useGettext } from 'vue3-gettext';
+
+// Needs to be unique within all installed applications in any OpenCloud
+// web instance. Should be short, unique and expressive as it is used as
+// prefix on all routes within your application.
+const appId = 'your-app'
+
+export default defineWebApplication({
+ setup({ applicationConfig }) {
+ // Here, you have access to the full injection context.
+ const { $gettext } = useGettext();
+
+ return {
+ appInfo: {
+ name: $gettext('Your application name'),
+ id: appId,
+ icon: 'aliens' // See https://remixicon.com
+ },
+ navItems: [ ... ],
+ routes: [ ... ],
+ extensions: [ ... ],
+ extensionPoints: [ ... ],
+ translations: { ... }
+ }
+ }
+})
+```
+
+`defineWebApplication` accepts the following keys:
+
+- `appInfo` - the application metadata. It makes the application available via the app switcher and the app registry.
+- `navItems` - the statically defined navigation items for the left sidebar. They only get rendered when more than 1
+ navigation item exists at runtime. Additional dynamic navigation items can be registered via the extension registry.
+- `routes` - the routes to the different views of your application. They may be referenced within the `navItems`.
+ Authentication requirements can be defined per item.
+- `extensions` - the extensions to be registered in the extension registry. For details, please refer to the
+ [extensions docs](./extensions/).
+- `extensionPoints` - the extension points to be registered in the extension registry. For details, please refer to the
+ [extension points docs](./extensions/extension-points).
+- `translations` - the translations of your application. For details, please refer to the
+ [translations docs](./advanced-topics/translations).
+
+## Vite configuration
+
+Apps are built with [Vite](https://vite.dev/). The `@opencloud-eu/extension-sdk` package provides a ready to use Vite
+config, so your `vite.config.ts` stays short:
+
+```typescript title="vite.config.ts"
+import { defineConfig } from '@opencloud-eu/extension-sdk';
+
+export default defineConfig({
+ name: 'my-app'
+});
+```
+
+`defineConfig` accepts any [Vite option](https://vite.dev/config/), plus the following:
+
+- `name` - The name of your app. Defaults to the `name` field of your `package.json`.
+- `opencloudWebHostUrl` - The URL of the OpenCloud Web dev server. Defaults to `https://host.docker.internal:9201`.
+
+The config sets up Vue, [Tailwind CSS](./advanced-topics/styling#tailwind-css), module federation and the generation of
+`manifest.json`. It also declares the modules that the OpenCloud Web runtime shares with your app, such as `vue`, `pinia`,
+`@opencloud-eu/web-pkg` and `@opencloud-eu/web-client`. These modules must not be bundled into your app.
+
+The following environment variables are supported:
+
+| Variable | Description |
+| ------------------------------ | -------------------------------------------------------------------- |
+| `OPENCLOUD_WEB_HOST_URL` | URL of the OpenCloud Web dev server. Same as `opencloudWebHostUrl`. |
+| `OPENCLOUD_EXTENSION_DIST_DIR` | Output directory of the build. Defaults to `dist`. |
+| `OPENCLOUD_CERTS_DIR` | Directory with a `server.key` and a `server.crt` for the dev server. |
+
+## What's next?
+
+- [Build and publish](./build-and-publish) your app, so that users can install it.
+- Register [extensions](./extensions/) to add functionality to existing places of the OpenCloud Web user interface.
+- Make your app [configurable](./advanced-topics/configuration) for administrators.
+- Follow the platform look with the [styling docs](./advanced-topics/styling).
+- Add [translations](./advanced-topics/translations) to your app.
+- Write [tests](./advanced-topics/testing) for your app.
+- Build a [viewer or editor app](./viewer-editor-apps) for specific file types.
+- Reuse our [helpful packages](./packages) instead of writing your own utilities.
diff --git a/docs/dev/web/extension-system/index.md b/docs/dev/web/extension-system/index.md
index 5962d994a..8e8a1a772 100644
--- a/docs/dev/web/extension-system/index.md
+++ b/docs/dev/web/extension-system/index.md
@@ -1,188 +1,33 @@
---
-title: 'Extension system'
+title: 'Extension System'
---
-## Concepts and Building Blocks
+OpenCloud Web can be extended with **apps**. An app is an artifact that gets installed in an OpenCloud instance.
+It is the main building block of the extension system: everything you add to OpenCloud Web is delivered as an app.
-OpenCloud Web can be extended through various entry points with custom **apps** and **extensions**.
+An app can do two things, and both of them are optional:
-### Distinction between Apps and Extensions
+1. It can take over the full app viewport, meaning everything below the top bar. There you can render any custom
+ application code, define views with routes, add navigation items to the left sidebar, and more.
+2. It can register [extensions](./extensions/). Extensions are small, focused pieces of functionality that get mounted
+ into predefined places of the OpenCloud Web user interface, for example a file action or a panel in the right sidebar.
-An Application in the context of OpenCloud Web is an artifact which can be installed in an OpenCloud instance.
-It serves two main purposes:
+This means an app can be a file editor without any extensions, a pure extension host without any custom views, or a
+combination of both.
-1. It makes the full app viewport (everything below the top bar) available to the application developer for any custom
- application code. This includes the ability to define views with routes, navigation items for the left sidebar, and more.
-2. Through the `extensions` key in the application interface you can register extensions of any extension type. Those extensions
- are then available in standardized extension points. Additionally, they can be queried from the extension registry for
- your own purposes.
+## Creating an app
-Both parts are optional. This means that an application can be a file editor without any custom extensions, or even contain
-no custom application code at all and only host extensions to be registered in the extension registry, or a combination of both.
+The [getting started guide](./getting-started) takes you from an empty folder to a running app. It is the entrypoint
+for app development.
-### Examples
+## Examples
-You can find open source examples for apps and extensions in our [curated list of OpenCloud apps and extensions](https://github.com/opencloud-eu/awesome-apps).
-Feel free to contribute or just be inspired for your own apps or extensions.
+You can find open source examples for apps and extensions in our
+[curated list of OpenCloud apps and extensions](https://github.com/opencloud-eu/awesome-apps).
+Feel free to contribute or just be inspired for your own apps and extensions.
-### Apps
+## Installing an app
-Apps define a `src/index.ts` file which acts as an entrypoint. Below is the most basic example of its content:
-
-```typescript
-import {
- AppWrapperRoute,
- ApplicationFileExtension,
- defineWebApplication
-} from '@opencloud-eu/web-pkg'
-
-
-export default defineWebApplication({
- setup({ applicationConfig }) {
- // Here, you have access to the full injection context, meaning you can use all composables that we provide via web-pkg
-
- // Needs to be unique within all installed applications in any OpenCloud web instance
- // Should be short, unique and expressive as it is used as prefix on all routes within your application
- const appId = 'your-extension'
-
- // See extensions section below
- const extensions = [
- ...
- ]
-
- // See details below
- const navItems = [
- ...
- ]
-
- // See details below
- const routes = [
- ...
- ]
-
- return {
- appInfo: {
- name: $gettext('Your application name'),
- id: appId,
- icon: 'aliens', // See https://remixicon.com/ for available options
- },
- extensions,
- navItems,
- routes
- }
- }
-})
-```
-
-By defining an application via `defineWebApplication` you can provide the following:
-
-- `appInfo` - the application metadata, which is used to make the application available via the app switcher and the app registry.
-- `navItems` - the statically defined navigation items for the left sidebar. Only gets rendered when more than 1 navigation item exists at runtime.
- Additional dynamic navigation items can be registered via the extension registry.
-- `routes` - the routes to the different views of your application. May be referenced within the `navItems`. Authentication requirements can be defined per item.
-- `extensions` - the extensions to be registered in the extension registry. For more details see the [Extensions](#extensions) section below.
-
-#### Creating a new app
-
-Please check out the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) for a boilerplate application to get started with your own application. In addition to that, if you want to learn how to implement an app for viewing and editing specific file types, please consult the [relevant documentation](./viewer-editor-apps) for detailed instructions and guidance.
-
-To learn how to integrate an app into OpenCloud Web, please refer to the [Web Application admin docs](../../../admin/configuration/web-applications).
-
-### Extensions
-
-In contrast to applications, extensions usually have a rather small scope and dedicated functionality.
-
-#### Extension Registry
-
-The globally available extension registry provided by the OpenCloud Web runtime can be used to both register and query extensions. All extensions
-which are being made available via an `app` get registered in the extension registry automatically. In your custom application code you can
-then query any of the available extensions by providing an `extensionPoint` entity. Throughout the OpenCloud Web platform
-and most prominently also in the `files` app we have defined some extension points which automatically use certain extensions, see the
-`Extension Points` section below.
-
-#### Extension Types
-
-For building an extension you can choose from the types predefined by the OpenCloud Web extension system. See the full list of available extension types below.
-
-1. `ActionExtension` (type `action`) - An extension that can register `Action` items which then get shown in various places (e.g. context menus, batch actions), depending on the
- extension points referenced in the extension respectively. Most commonly used for file and folder actions (e.g. copy, rename, delete, etc.). For details, please refer to the [action docs](./extension-types/action-extensions)
-2. `SearchExtension` (type `search`) - An extension that can register additional search providers. For details, please refer to the [search docs](./extension-types/search-extensions).
-3. `SidebarNavExtension` (type `sidebarNav`) - An extension that can register additional navigation items for the left sidebar. These can be scoped to specific apps, and programmatically enabled/disabled.
- For details, please refer to the [sidebar nav docs](./extension-types/left-sidebar-menu-item-extensions).
-4. `SidebarPanelExtension`, (type `sidebarPanel`) - An extension that can register panels for the right sidebar. For details, please refer to the [sidebar panel docs](./extension-types/right-sidebar-panel-extensions).
-5. `FolderViewExtension` (type `folderView`) - An extension that can register additional ways of displaying the content of a folder (resources like spaces, folders or files) to the user.
- For details, please refer to the [folder view docs](./extension-types/folder-view-extensions).
-6. `CustomComponentExtension` (type `customComponent`) - An extension that can register a custom component for a render target. For details, please refer to the
- [custom component docs](./extension-types/custom-component-extensions)
-7. `FloatingActionButtonExtension` (type `floatingActionButton`) - An extension that can register one or multiple actions, either displayed within the left sidebar (for desktop resolutions) or as a floating action button (for mobile resolutions). For details, please refer to the
- [floating action button docs](./extension-types/floating-action-button-extensions).
-
-You're free to introduce your own extension types within your application code and use the extension registry to query the available ones. However, if you have the impression
-that an important extension type is missing and would be beneficial for the platform, please reach out to us by opening a [GitHub issue](https://github.com/opencloud-eu/web/issues/new/choose).
-
-#### Extension Base Configuration
-
-Any extension is required to define at least an `id` and a `type` in order to fulfill the generic `Extension` interface.
-
-The `id` is supposed to be unique throughout the OpenCloud Web ecosystem. In order to keep `id`s readable for humans we didn't want to enforce uniqueness through e.g. uuids.
-Instead, we chose to use dot-formatted namespaces like e.g. `com.github.opencloud-eu.web.files.search`. We'd like to encourage you to follow the same format for your own extensions.
-
-For the `type` you can choose from the ones listed above or define a custom one.
-
-In addition, you can also pass optional `extensionPointIds` to further limit the usage of an extension. With the right click context menu and the batch actions being
-two different extension points, this could mean that a file action extension is only allowed in the context menu, but not in the batch actions.
-You can find predefined extension point ids in the extension points section below.
-
-#### Extension Points
-
-There are standardized components and places where extensions are being used automatically. The following ones are currently provided by the OpenCloud Web runtime or
-the `files` app. If you decide to develop an extension which fulfills the type and registers itself for the extensionPointId of the respective extension point,
-your extension will be used automatically.
-
-1. Left Sidebar for Navigation.
- 1. Floating Action Button (FAB). ExtensionPointId `app.${appName}.floating-action-button` (dynamically created for each app). Mounts extensions of type `floatingActionButton`.
- 2. Additional nav items. ExtensionPointId `app.${appName}.navItems` (dynamically created for each app). Mounts extensions of type `sidebarNav`.
- 3. Sidebar main area below nav items. ExtensionPointId `app.${appName}.sidebar-nav.main` (dynamically created for each app). Mounts extensions of type `customComponent`.
- 4. Sidebar bottom area above version info. ExtensionPointId `app.${appName}.sidebar-nav.bottom` (dynamically created for each app). Mounts extensions of type `customComponent`.
-2. Global top bar
- 1. Center area. ExtensionPointId `app.runtime.header.center`. Mounts extensions of type `customComponent`.
- 2. Left area. ExtensionPointId `app.runtime.header.left`. Mounts extensions of type `customComponent`.
- 3. Right area. ExtensionPointId `app.runtime.header.right`. Mounts extensions of type `customComponent`.
- 4. Progress bar for the global loading state. ExtensionPointId `app.runtime.global-progress-bar`. Mounts a single extension of type `customComponent`. If multiple exist, the user can choose via the account page.
-3. Files app
- 1. Right sidebar.
- 1. Panels. ExtensionPointId `app.files.sidebar`. Mounts extensions of type `sidebarPanel`. Used in any file(s) context (files app, file viewer apps, file editor apps).
- 2. File details table. ExtensionPointId `app.files.sidebar.file-details.table`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 3. Space details table. ExtensionPointId `app.files.sidebar.space-details.table`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 4. Shares panel people list top section. ExtensionPointId `app.files.sidebar.shares-panel.shared-with.top`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 5. Shares panel people list bottom section. ExtensionPointId `app.files.sidebar.shares-panel.shared-with.bottom`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 2. Folder views for regular folders. ExtensionPointId `app.files.folder-views.folder`. Mounts extensions of type `folderView`.
- 3. Folder views for the project spaces overview. ExtensionPointId `app.files.folder-views.project-spaces`. Mounts extensions of type `folderView`.
- 4. Folder views for the favorites page. ExtensionPointId `app.files.folder-views.favorites`. Mounts extensions of type `folderView`.
- 5. Right click context menu. ExtensionPointId `global.files.context-actions`. Mounts extensions of type `action`.
- 6. Batch actions in the app bar above file lists. ExtensionPointId `global.files.batch-actions`. Mounts extensions of type `action`.
- 7. Default actions (left click) on a file. ExtensionPointId `global.files.default-actions`. Mounts extensions of type `action`.
- 8. Upload menu. ExtensionPointId `app.files.upload-menu`. Mounts extensions of type `action`.
- 9. Quick actions. ExtensionPointId `app.files.quick-actions`. Mounts extensions of type `action`.
- 10. Quick actions for the trash overview. ExtensionPointId `app.files.trash-quick-actions`. Mounts extensions of type `action`.
-4. Global search providers. ExtensionPointId `app.search.providers`. Utilizes extensions of type `search` as search engines for the search input in the global top bar.
-5. User preference panels. ExtensionPointId `app.runtime.preferences.panels`. Mounts extensions of type `customComponent`.
-
-#### User Preferences for Extensions
-
-To allow users to configure extensions, extension points can define user preferences. User preferences are defined as an object on the extension point configuration.
-Whenever an extension point declares to accept user preferences, it will get listed with a dropdown on the Preferences page (reachable via top right user menu).
-The user can then select one out of all the extensions which have been registered for this extension point.
-
-#### Creating a new extension
-
-Please check out the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) for a boilerplate application that also includes an extension. In addition to that, the [extension types docs](./extension-types/) provide instructions and examples on how to implement the different extension types.
-
-### Helpful packages
-
-We currently offer the following packages that can be integrated into your app, providing useful utilities and types.
-
-- `extension-sdk` - This package provides a default vite config that can be used when developing applications and extensions for the OpenCloud Web ecosystem. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/extension-sdk/README.md).
-- `web-client` - This package serves as an abstraction layer between the server APIs and an app or extension. It converts raw API data into objects with helpful types and utilities. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-client/README.md).
-- `web-pkg` - This package provides utilities, most importantly a variety of components and composables, that can be useful when developing apps and extensions. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-pkg/README.md).
-- `web-test-helpers` - This package provides utilities for writing unit tests. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-test-helpers/README.md).
+To learn how to integrate an app into OpenCloud Web, please refer to the
+[Web application admin docs](../../../admin/configuration/web-applications). To learn how to ship your app to users,
+please refer to the [build and publish docs](./build-and-publish).
diff --git a/docs/dev/web/extension-system/packages.md b/docs/dev/web/extension-system/packages.md
new file mode 100644
index 000000000..bf32a9a74
--- /dev/null
+++ b/docs/dev/web/extension-system/packages.md
@@ -0,0 +1,21 @@
+---
+title: 'Helpful packages'
+sidebar_position: 6
+---
+
+We currently offer the following packages that can be integrated into your app, providing useful utilities and types.
+All of them are published on npm under the `@opencloud-eu` scope.
+
+## Runtime
+
+- `web-pkg` - This package provides utilities, most importantly a variety of components and composables, that can be useful when developing apps and extensions. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-pkg/README.md).
+- `web-client` - This package serves as an abstraction layer between the server APIs and an app or extension. It converts raw API data into objects with helpful types and utilities. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-client/README.md).
+- `design-system` - This package provides the components and utilities that OpenCloud Web is built with. Use it to make your app look and behave like the rest of the platform. For details, please refer to the [design system documentation](../design-system).
+
+## Tooling
+
+- `extension-sdk` - This package provides a default vite config that can be used when developing apps and extensions for the OpenCloud Web ecosystem. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/extension-sdk/README.md).
+- `tsconfig` - This package provides a reusable TypeScript config. Extend it in your `tsconfig.json`. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/tsconfig/README.md).
+- `eslint-config` - This package provides a reusable ESLint config. Extend it in your `eslint.config.js`. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/eslint-config/README.md).
+- `prettier-config` - This package provides a reusable Prettier config. Extend it in your `.prettierrc.json`. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/prettier-config/README.md).
+- `web-test-helpers` - This package provides utilities for writing unit tests. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-test-helpers/README.md).
diff --git a/docs/dev/web/extension-system/viewer-editor-apps/_category_.json b/docs/dev/web/extension-system/viewer-editor-apps/_category_.json
new file mode 100644
index 000000000..30fd88bbc
--- /dev/null
+++ b/docs/dev/web/extension-system/viewer-editor-apps/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Viewer and editor apps",
+ "position": 5
+}
diff --git a/docs/dev/web/extension-system/viewer-editor-apps/collaborative-editing.md b/docs/dev/web/extension-system/viewer-editor-apps/collaborative-editing.md
new file mode 100644
index 000000000..f1bcb0a23
--- /dev/null
+++ b/docs/dev/web/extension-system/viewer-editor-apps/collaborative-editing.md
@@ -0,0 +1,255 @@
+---
+title: 'Collaborative editing'
+sidebar_position: 1
+---
+
+OpenCloud Web uses [Yjs](https://yjs.dev/) for real-time collaborative editing, which allows multiple users to edit the
+same document simultaneously while ensuring that all changes are consistently merged. Yjs is a CRDT framework. Every
+client holds its own replica of the document, and all replicas converge to the same state.
+
+An editor app opts into collaboration with one route option and one adapter. Everything else - loading, saving, etag
+handling and conflict resolution - stays abstract in the `AppWrapper`.
+
+## How it fits together
+
+```mermaid
+flowchart LR
+ A["Client Ayour app "]
+ B["Client Byour app "]
+ Y["Yjs serverin-memory relay "]
+ D["WebDAV"]
+
+ A <-->|"CRDT updates + awareness"| Y
+ B <-->|"CRDT updates + awareness"| Y
+ A -->|"GET / PUT"| D
+ B -->|"GET / PUT"| D
+```
+
+The Yjs server only relays updates between clients in the same room. It never reads or writes the file, and it keeps no
+state after the last client leaves. The file is always loaded and saved by the browser over WebDAV.
+
+One room is one opened file. The room name contains the file id, an app prefix and the Web version. Apps with different
+Y.Doc layouts therefore never share a room.
+
+## Requirements
+
+Collaborative editing is only active if the deployment sets `WEB_OPTION_YJS_SERVER_URL`. See the
+[admin documentation](../../../../admin/configuration/collaborative-editing) for the server setup.
+
+The Yjs server authenticates the user with a bearer token against the LibreGraph API, and it checks the permissions of
+the file. Contexts without a user token, for example public links or OCM, always fall back to local mode.
+
+## Turning it on in your app
+
+Collaboration is a `yjs` option on `AppWrapperRoute`. The rest of the app setup is the same as for any other editor
+app:
+
+```typescript title="src/index.ts"
+import { AppWrapperRoute, defineWebApplication } from '@opencloud-eu/web-pkg';
+import { useGettext } from 'vue3-gettext';
+import App from './App.vue';
+import { makeMyAdapter } from './yjs';
+
+export default defineWebApplication({
+ setup() {
+ const { $gettext } = useGettext();
+ const appId = 'my-editor';
+
+ const routes = [
+ {
+ name: appId,
+ path: '/:driveAliasAndItem(.*)?',
+ component: AppWrapperRoute(App, {
+ applicationId: appId,
+ yjs: {
+ // Builds the bridge between the file format and the shared Y.Doc.
+ makeAdapter: makeMyAdapter,
+
+ // Optional. Room namespace, defaults to the applicationId.
+ documentPrefix: appId
+ }
+ }),
+ meta: {
+ authContext: 'hybrid',
+ title: $gettext('My Editor'),
+ patchCleanPath: true
+ }
+ }
+ ];
+
+ return {
+ appInfo: {
+ name: 'My Editor',
+ id: appId,
+ defaultExtension: 'md',
+ extensions: [{ extension: 'md', routeName: appId }]
+ },
+ routes
+ };
+ }
+});
+```
+
+## The adapter
+
+The Yjs session is generic. It handles the connection, the hydration and the etag loop. The adapter tells it how to move
+content between the native file format and the Y.Doc.
+
+```typescript
+interface YjsAdapter {
+ /** Seed an empty Y.Doc from the file content. Must be a no-op if the doc already has content. */
+ hydrate(ydoc: Y.Doc, content: string): void;
+
+ /** Render the current Y.Doc state back to the native file format. */
+ serialize(ydoc: Y.Doc): string | Promise;
+
+ /** True if the Y.Doc already holds app content. */
+ hasContent(ydoc: Y.Doc): boolean;
+
+ /** Optional. Wipe the shared content so `hasContent` returns false again. */
+ reset?(ydoc: Y.Doc): void;
+}
+```
+
+Rules to keep in mind:
+
+- `hydrate` must be synchronous. Stale recovery wipes and re-seeds the document in one go.
+- `serialize` runs on every peer after each pause in typing. Keep it cheap, and do not build an editor instance in it.
+- `makeAdapter` runs during the setup of the `AppWrapper`, before the file is loaded. It gets a reactive context
+ `{ resource: Ref }` and must read it lazily. Because it runs in setup, it may use composables.
+
+### Tiptap based apps
+
+If your editor is built on Tiptap, use `makeTiptapYjsAdapter`. It takes a content strategy and returns a ready adapter:
+
+```typescript title="src/yjs.ts"
+import { ref } from 'vue';
+import type { YjsAdapter, YjsAdapterContext } from '@opencloud-eu/web-pkg';
+import { makeTiptapYjsAdapter, useContentStrategy } from '@opencloud-eu/web-pkg/editor';
+
+export function makeMyAdapter({ resource }: YjsAdapterContext): YjsAdapter {
+ const { resolveStrategy } = useContentStrategy();
+
+ const strategy = resolveStrategy('markdown', {
+ sourceMode: ref(false),
+ linkPanel: ref(null),
+ editorZoom: ref(100),
+ currentResource: resource
+ });
+
+ return makeTiptapYjsAdapter(strategy);
+}
+```
+
+The strategy must be resolved in setup, so resolve it here and not inside the adapter methods. To support several
+content types, resolve one strategy per type and pass a getter that picks by `resource`.
+
+The content lives in a `Y.XmlFragment` named `default`. `Collaboration` from Tiptap writes into it.
+
+## The app component
+
+The `AppWrapper` passes the session down as slot props. Declare the props you need. `YjsEditorSlotProps` is a preset for
+collaborative editors:
+
+```typescript
+type YjsEditorSlotProps = {
+ resource: Resource;
+ space: SpaceResource;
+ currentContent: string;
+ isReadOnly: boolean;
+
+ /** The shared document. */
+ ydoc: Y.Doc | null;
+
+ /** Peer presence: cursors, selections and user identity. */
+ awareness: Awareness | null;
+
+ /** 'connecting' | 'connected' | 'disconnected' | 'local' */
+ yjsStatus: YjsStatus | null;
+};
+```
+
+The `AppWrapper` keeps its loading screen up until the session is synced and hydrated. Your component therefore mounts
+against a Y.Doc that is ready, and `ydoc` and `awareness` are never null inside it.
+
+```html title="src/App.vue"
+
+```
+
+`useTextEditor` binds Tiptap to the Y.Doc and renders the remote cursors. If you build your own editor, bind it to
+`ydoc` yourself, with the binding your editor library provides.
+
+### Showing the collaborators
+
+`useYjsCollaborators` turns the awareness states into a list of users. The own user comes first, peers follow sorted by
+name. A user with several tabs open appears once.
+
+```typescript
+import { useYjsCollaborators } from '@opencloud-eu/web-pkg';
+
+const collaborators = useYjsCollaborators(awareness);
+// [{ id, name, color, isSelf }]
+```
+
+The identity and the color come from the Yjs server, not from the client. A client cannot present itself as someone
+else.
+
+## Saving
+
+Saving does not change. The `AppWrapper` writes the file over WebDAV with `If-Match`, either on `Ctrl+S` or through the
+autosave timer. Collaboration only changes where the content comes from:
+
+1. A user types, the update reaches every peer.
+2. 300 ms after typing stops, the session calls `adapter.serialize` and updates `currentContent`.
+3. `isDirty` flips to true. No request is sent yet.
+4. On save, the `AppWrapper` sends the `PUT` and shares the new etag with the room.
+5. Peers whose edits are covered by that write become clean again.
+
+If a save comes back with a conflict, the wrapper first checks whether the conflicting write came from the same room. If
+it did, it retries. If an external client wrote the file, the user gets the conflict dialog.
+
+## Local mode
+
+If no Yjs server URL is configured, the session still creates a Y.Doc and an awareness object, but it skips the
+connection and hydrates at once. The status is `local`.
+
+The same happens if the server cannot be reached. After a timeout, the session gives up, hydrates locally and shows an
+error saying that changes are not shared. The file stays editable and can be saved.
+
+Your binding is the same in both modes, so you do not need a branch for it.
+
+## Read-only users
+
+A user without write permission connects in read-only mode. The Yjs server rejects all their updates. The
+`AppWrapper` sets `isReadOnly`, and `isDirty` stays false for them.
+
+## Limits
+
+- Nothing is saved without an open browser. If all clients close between autosaves, the edits since the last save are
+ lost.
+- Every peer autosaves, so one document is saved once per open client per interval.
+- Remote edits mark your app dirty, so the unsaved-changes guard can fire for edits you did not make.
+- Different Web versions do not share a room. During a rolling upgrade, users end up in separate rooms.
+
+For the full architecture, the hydration election and the stale recovery, see the
+[Yjs developer doc](https://github.com/opencloud-eu/web/blob/main/dev/docs/yjs.md) in the web repository.
+
+## Example
+
+The [Excalidraw app](https://github.com/opencloud-eu/web-extensions/tree/main/packages/web-app-excalidraw) in the
+`web-extensions` repository is a complete example. It uses its own adapter and its own Y.Doc layout, not Tiptap.
diff --git a/docs/dev/web/extension-system/viewer-editor-apps.md b/docs/dev/web/extension-system/viewer-editor-apps/index.md
similarity index 73%
rename from docs/dev/web/extension-system/viewer-editor-apps.md
rename to docs/dev/web/extension-system/viewer-editor-apps/index.md
index b70535907..11ef30ecc 100644
--- a/docs/dev/web/extension-system/viewer-editor-apps.md
+++ b/docs/dev/web/extension-system/viewer-editor-apps/index.md
@@ -1,28 +1,18 @@
---
title: 'Viewer and editor apps'
-sidebar_position: 1
---
-## Viewer and editor apps
-
OpenCloud Web allows developers to implement apps for viewing and editing specific file types. For instance, the built-in preview app serves as the default application for opening media files like images, videos, or audio.
This section will guide you through the process of implementing such an app within OpenCloud Web.
-### Basic app structure
-
-An app is essentially a distinct package that must be specified as an external application in the Web configuration.
-
-The structure of an app is quite simple and straightforward. Consider, for example, the [pdf-viewer app](https://github.com/opencloud-eu/web/tree/main/packages/web-app-pdf-viewer). It consists of a `package.json` file, a `src` directory containing all the source code, and a `l10n` directory for translations. Optionally, you may also include a `tests` directory if your application requires testing.
-
-To learn more about apps in general, please refer to the [Web app docs](../extension-system/).
+## App setup
-### App setup
+The `src/index.ts` file for a viewer or editor app may look like this:
-Inside the `src` folder you will need an `index.ts` file that sets up the app so it can be registered by the Web runtime. It follows the basic structure as described in [the apps section](../extension-system/), so it may look like this:
-
-```typescript
+```typescript title="src/index.ts"
import { AppWrapperRoute, defineWebApplication, AppMenuItemExtension } from '@opencloud-eu/web-pkg';
+import { urlJoin } from '@opencloud-eu/web-client';
import translations from '../l10n/translations.json';
import { useGettext } from 'vue3-gettext';
import { computed } from 'vue';
@@ -57,11 +47,13 @@ export default defineWebApplication({
// if you want your app to be present in the app menu on the top left.
const menuItems = computed(() => [
{
+ id: `app.${appId}.menuItem`,
label: () => $gettext('Advanced PDF Viewer'),
type: 'appMenuItem',
- handler: () => {
- // do stuff...
- }
+ color: '#ffffff',
+ icon: 'file-pdf',
+ priority: 30,
+ path: urlJoin(appId)
}
]);
diff --git a/docs/dev/web/getting-started.md b/docs/dev/web/getting-started.md
index 80a5b8dc4..58dc706eb 100644
--- a/docs/dev/web/getting-started.md
+++ b/docs/dev/web/getting-started.md
@@ -13,7 +13,9 @@ To install and setup the Web client on your local machine, please refer to the [
## Configuration
-Web can be configured using a configuration file in `json` format. Sample configuration files are available in the [config folder](https://github.com/opencloud-eu/web/tree/main/config) of the OpenCloud Web Git repository. Below is a detailed overview of all available configuration options.
+Web can be configured using a configuration file in `json` format. This is completely optional, as default settings are used when no configuration file is provided. You need to tell the server where to find a custom configuration file via the `WEB_UI_CONFIG_FILE` environment variable.
+
+Below is a detailed overview of all available configuration options.
### `server`
@@ -23,30 +25,6 @@ Specifies the server URL, e.g. `https://host.docker.internal:9200`.
Specifies the URL for the theme to be loaded, e.g. `https://host.docker.internal:9200/themes/opencloud/theme.json`.
-### `options`
-
-General options that control the behavior of the Web client. Expects an object with the following possible options:
-
-- `options.accountEditLink` This accepts an object with the following optional fields to have a link on the account page:
- - `options.accountEditLink.href` Set a different target URL for the edit link. Make sure to prepend it with `http(s)://`.
-- `options.sharingRecipientsPerPage` Sets the amount of users shown as recipients in the dropdown when sharing resources. Default amount is 200.
-- `options.runningOnEos` Set this option to `true` if running on an [EOS storage backend](https://eos-web.web.cern.ch/eos-web/) to enable its specific features. Defaults to `false`.
-- `options.cernFeatures` Enabling this will activate CERN-specific features. Defaults to `false`.
-- `options.editor.autosaveEnabled` Specifies if the autosave for the editor apps is enabled.
-- `options.editor.autosaveInterval` Specifies the time interval for the autosave of editor apps in seconds.
-- `options.editor.openAsPreview` Specifies if non-personal files i.e. files in shares, spaces or public links are being opened in read only mode so the user needs to manually switch to edit mode. Can be set to `true`, `false` or an array of web app/editor names.
-- `options.contextHelpersReadMore` Specifies whether the "Read more" link should be displayed or not.
-- `options.tokenStorageLocal` Specifies whether the access token will be stored in the local storage when set to `true` or in the session storage when set to `false`. If stored in the local storage, login state will be persisted across multiple browser tabs, means no additional logins are required. Defaults to `true`.
-- `options.loginUrl` Specifies the target URL to the login page. This is helpful when an external IdP is used. This option is disabled by default. Example URL like: '[https://www.myidp.com/login](https://www.myidp.com/login)'.
-- `options.logoutUrl` Adds a link to the user's profile page to point him to an external page, where he can manage his session and devices. This is helpful when an external IdP is used. This option is disabled by default.
-- `options.userListRequiresFilter` Defines whether one or more filters must be set in order to list users in the Web admin settings. Set this option to 'true' if running in an environment with a lot of users and listing all users could slow down performance. Defaults to `false`.
-- `options.concurrentRequests` This accepts an object with the following optional fields to customize the maximum number of concurrent requests in code paths where we limit concurrent requests
- - `resourceBatchActions` Concurrent number of file/folder/space batch actions like e.g. accepting shares. Defaults to 4.
- - `sse` Concurrent number of SSE event handlers. Defaults to 4.
- - `shares` Accepts an object regarding the following sharing related options:
- - `create` Concurrent number of share invites. Defaults to 4.
- - `list` Concurrent number of individually loaded shares. Defaults to 2.
-
### `apps`
Controls the Web apps to be loaded. This is not for adding external apps, but for specifying which of the internal apps that are shipped with Web should be loaded. Expects a list of strings, e.g.:
diff --git a/docs/dev/web/testing/_category_.json b/docs/dev/web/testing/_category_.json
index 296ff9a78..22d3090ec 100644
--- a/docs/dev/web/testing/_category_.json
+++ b/docs/dev/web/testing/_category_.json
@@ -1,9 +1,4 @@
{
"label": "Testing",
- "position": 3,
- "link": {
- "type": "generated-index",
- "description": "✅ Testing",
- "slug": "dev/web/testing/"
- }
+ "position": 3
}
diff --git a/docs/dev/web/testing/index.md b/docs/dev/web/testing/index.md
new file mode 100644
index 000000000..db02b0441
--- /dev/null
+++ b/docs/dev/web/testing/index.md
@@ -0,0 +1,7 @@
+---
+title: 'Testing'
+---
+
+This section describes the testing practices used for OpenCloud Web.
+
+See the [end-to-end testing standards](./e2e-testing-standards) for guidance on writing and organizing Playwright tests.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/_category_.json b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/_category_.json
new file mode 100644
index 000000000..c1aec597f
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Advanced topics",
+ "position": 3
+}
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/configuration.md b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/configuration.md
new file mode 100644
index 000000000..84024cd6b
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/configuration.md
@@ -0,0 +1,56 @@
+---
+title: 'Configuration'
+sidebar_position: 1
+---
+
+This page describes how to add configuration options to your app. Administrators can then change the behaviour of your
+app.
+
+## Manifest and defaults
+
+Put static metadata of your app into `src/manifest.json`. It ends up in the `manifest.json` of your build, together with
+the entry point of your app.
+
+Default values for the app config belong under the `config` key:
+
+```json title="src/manifest.json"
+{
+ "config": {
+ "maxFileSize": 10485760,
+ "showPreview": true
+ }
+}
+```
+
+## Overriding values
+
+There are two ways to override the defaults:
+
+- During development, put your values into `src/config.json`. The file is merged on top of the `config` key of
+ `src/manifest.json`. Don't commit values that only apply to your machine.
+- In a real deployment, administrators override them via the `apps.yaml` file, see the
+ [web applications admin docs](../../../../admin/configuration/web-applications).
+
+```json title="src/config.json"
+{
+ "showPreview": false
+}
+```
+
+## Reading values in your app
+
+The merged config is passed to the `setup` function of your app definition as `applicationConfig`:
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg';
+
+export default defineWebApplication({
+ setup({ applicationConfig }) {
+ const showPreview = applicationConfig?.showPreview ?? true;
+
+ // ...
+ }
+});
+```
+
+Always provide a fallback value. The config can be empty, and an administrator can set any value.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/styling.md b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/styling.md
new file mode 100644
index 000000000..8e5e403a0
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/styling.md
@@ -0,0 +1,52 @@
+---
+title: 'Styling'
+sidebar_position: 2
+---
+
+This page describes how to style your app so that it fits into OpenCloud Web.
+
+## Design system components
+
+The `@opencloud-eu/design-system` package contains the components that OpenCloud Web is built with, for example buttons,
+inputs and modals. Use them where possible. Your app then follows the platform look and inherits accessibility and
+theming for free. See the [design system documentation](../../design-system) for the list of components.
+
+## Color roles
+
+OpenCloud Web exposes its colors as CSS variables with the `--oc-role-` prefix, for example `--oc-role-surface` or
+`--oc-role-on-surface`. Always use these variables instead of fixed color values. They change with the active theme and
+with the color scheme, so your app stays readable in light and dark mode.
+
+```css
+.my-panel {
+ background-color: var(--oc-role-surface-container);
+ color: var(--oc-role-on-surface);
+}
+```
+
+See [the defaults](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/design-system/src/styles/defaults.css) for a
+complete list of color roles.
+
+## Tailwind CSS
+
+The `@opencloud-eu/extension-sdk` package ships a preconfigured [Tailwind CSS](https://tailwindcss.com/) setup. Import
+its stylesheet in your entrypoint (typically `src/index.ts`) to use it:
+
+```typescript title="src/index.ts"
+import '@opencloud-eu/extension-sdk/tailwind.css';
+```
+
+Tailwind classes need the `ext:` prefix in apps. The prefix avoids style conflicts with the OpenCloud Web runtime.
+
+```html
+...
+```
+
+The setup maps the color roles to Tailwind color utilities, so `--oc-role-surface` is available as `bg-role-surface`,
+`text-role-on-surface` and so on:
+
+```html
+...
+```
+
+It also sets the OpenCloud font family, a spacing unit of `4px`, and the breakpoints `xs`, `sm`, `md`, `lg` and `xl`.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/testing.md b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/testing.md
new file mode 100644
index 000000000..5af9fc9d3
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/testing.md
@@ -0,0 +1,41 @@
+---
+title: 'Testing'
+sidebar_position: 4
+---
+
+This page describes how to test your app.
+
+## Unit tests
+
+The [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) comes with a
+[vitest](https://vitest.dev/) setup. Run the unit tests with:
+
+```bash
+pnpm test:unit
+```
+
+## Test helpers
+
+The `@opencloud-eu/web-test-helpers` package provides utilities for mounting components with a mocked OpenCloud Web
+context. Without them, every component that uses a composable of `web-pkg` fails to mount.
+
+```typescript title="tests/unit/App.spec.ts"
+import { defaultPlugins, mount } from '@opencloud-eu/web-test-helpers';
+import App from '../../src/App.vue';
+
+describe('App', () => {
+ it('renders the title', () => {
+ const wrapper = mount(App, { global: { plugins: [...defaultPlugins()] } });
+ expect(wrapper.text()).toContain('My app');
+ });
+});
+```
+
+For details, please refer to the package's
+[README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/web-test-helpers/README.md).
+
+## End to end tests
+
+For end to end tests with [Playwright](https://playwright.dev/), the
+[web](https://github.com/opencloud-eu/web) and [web-extensions](https://github.com/opencloud-eu/web-extensions)
+repositories contain working examples.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/translations.md b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/translations.md
new file mode 100644
index 000000000..0485a2686
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/advanced-topics/translations.md
@@ -0,0 +1,138 @@
+---
+title: 'Translations'
+sidebar_position: 3
+---
+
+OpenCloud Web uses [gettext](https://www.gnu.org/software/gettext/) for translations, via the
+[vue3-gettext](https://jshmrtn.github.io/vue3-gettext/) library. Your app brings its own translations and hands them to
+the Web runtime. The language that the user picked in OpenCloud is then also used for your app.
+
+Translations are optional. An app without them simply shows its original strings.
+
+## Marking strings
+
+In a script or a composable, get `$gettext` from `useGettext`:
+
+```typescript
+import { useGettext } from 'vue3-gettext';
+
+const { $gettext } = useGettext();
+
+const title = $gettext('Your application name');
+```
+
+In a template, `$gettext` is globally available:
+
+```html
+
+ {{ $gettext('No files here') }}
+
+```
+
+Pass variables as an object. Use the `%{name}` syntax in the string, never string concatenation, because the word order
+changes between languages:
+
+```typescript
+$gettext('Open «%{resource}»', { resource: resource.name });
+```
+
+For plurals, use `$ngettext` with the singular form, the plural form, the count, and the variables:
+
+```typescript
+const { $ngettext } = useGettext();
+
+$ngettext(
+ 'Delete the selected resource?',
+ 'Delete %{amount} selected resources?',
+ resources.length,
+ { amount: resources.length.toString() }
+);
+```
+
+:::warning
+The extraction tool reads your source code, not your runtime values. Always pass a plain string literal to `$gettext`.
+A variable or a template literal cannot be extracted.
+:::
+
+## Setting up the workflow
+
+Add [vue3-gettext](https://jshmrtn.github.io/vue3-gettext/) as a dev-dependency. It ships the `vue-gettext-extract` and
+`vue-gettext-compile` commands.
+
+Create a `gettext.config.cjs` in the root of your app. It defines which files are scanned and which languages you
+support:
+
+```javascript title="gettext.config.cjs"
+module.exports = {
+ input: {
+ path: './src',
+ include: ['**/*.js', '**/*.ts', '**/*.vue']
+ },
+ output: {
+ locales: ['de', 'es', 'fr', 'it'],
+ path: './l10n/locale',
+ potPath: '../template.pot',
+ jsonPath: '../translations.json',
+ flat: false,
+ linguas: false
+ }
+};
+```
+
+Add two scripts to your `package.json`:
+
+```json title="package.json"
+{
+ "scripts": {
+ "l10n:extract": "vue-gettext-extract",
+ "l10n:compile": "vue-gettext-compile"
+ }
+}
+```
+
+`l10n:extract` collects all marked strings into `l10n/template.pot` and creates one `.po` file per language under
+`l10n/locale`. Run it whenever you add or change a string.
+
+`l10n:compile` turns the `.po` files into a single `l10n/translations.json`. Run it after the translated `.po` files
+come back. Commit `translations.json`, because the build imports it.
+
+How the `.po` files get translated is up to you. The OpenCloud repositories use
+[Transifex](https://www.transifex.com/), but any gettext based service or a manual workflow works as well.
+
+## Registering translations
+
+Import `translations.json` and return it from your app definition. The Web runtime merges it into the global
+translations when your app is loaded.
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg';
+import { useGettext } from 'vue3-gettext';
+import translations from '../l10n/translations.json';
+
+export default defineWebApplication({
+ setup() {
+ const { $gettext } = useGettext();
+
+ return {
+ appInfo: {
+ name: $gettext('Your application name'),
+ id: 'your-app'
+ },
+ translations
+ };
+ }
+});
+```
+
+The file is a map of language key to message map:
+
+```json title="l10n/translations.json"
+{
+ "de": {
+ "No files here": "Keine Dateien hier"
+ },
+ "fr": {
+ "No files here": "Aucun fichier ici"
+ }
+}
+```
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/build-and-publish.md b/versioned_docs/version-7.2/dev/web/extension-system/build-and-publish.md
new file mode 100644
index 000000000..a98ff974b
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/build-and-publish.md
@@ -0,0 +1,102 @@
+---
+title: 'Build and publish'
+sidebar_position: 2
+---
+
+This page describes how to build your app for production and how to publish it in the OpenCloud app store.
+
+## Building an app
+
+Run a production build:
+
+```bash
+pnpm build
+```
+
+The build writes your app into the `dist` folder. Static assets from a `public` folder are copied over as is. The result
+contains:
+
+- `js/-.mjs` - the entry point of your app, plus its chunks.
+- `manifest.json` - the metadata that OpenCloud uses to discover and load your app.
+- any static assets of your app.
+
+The generated `manifest.json` is your `src/manifest.json` plus the `entrypoint` key, which the build sets. Only
+`entrypoint` is required by the server. Add any other metadata of your app to `src/manifest.json` yourself.
+
+```json title="dist/manifest.json"
+{
+ "name": "my-app",
+ "version": "1.0.0",
+ "description": "My OpenCloud app",
+ "entrypoint": "js/my-app-a1b2c3d4.mjs"
+}
+```
+
+## Publishing in the app store
+
+Apps in the OpenCloud app store are listed in the
+[awesome-apps repository](https://github.com/opencloud-eu/awesome-apps). Publishing means adding your app to its
+`webApps/apps.json` file via a pull request.
+
+### Requirements
+
+- Your app must be downloadable as a `.zip` file from a stable URL, for example a GitHub release asset.
+- The zip must contain a `manifest.json` in its root, next to your built files. A production build produces this
+ already, so zip the content of your `dist` folder, not the folder itself.
+
+### apps.json entry
+
+Add one entry under the `apps` key. The schema is defined in the
+[app store types](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/web-app-app-store/src/types.ts).
+
+```json title="webApps/apps.json"
+{
+ "id": "com.github.my-org.my-repo.my-app",
+ "name": "My App",
+ "subtitle": "One short line about what the app does.",
+ "description": "A longer description.",
+ "license": "Apache-2.0",
+ "versions": [
+ {
+ "version": "1.0.0",
+ "minOpenCloud": "7.2.0",
+ "url": "https://github.com/my-org/my-repo/releases/download/v1.0.0/my-app-1.0.0.zip"
+ }
+ ],
+ "authors": [{ "name": "My Organization", "url": "https://example.org" }],
+ "tags": ["editor", "viewer"],
+ "coverImage": {
+ "url": "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/main/webApps/my-org/my-repo/cover.png"
+ },
+ "screenshots": [
+ {
+ "url": "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/main/webApps/my-org/my-repo/screenshots/1.png",
+ "caption": "What this screenshot shows"
+ }
+ ],
+ "resources": [
+ { "url": "https://github.com/my-org/my-repo", "label": "Source code", "icon": "github" }
+ ]
+}
+```
+
+Notes on the fields:
+
+- `versions` must be sorted from newest to oldest. `minOpenCloud` is the lowest OpenCloud version your app supports.
+- `badge` is optional and accepts a `label` and a `color` of `primary`, `success` or `danger`.
+- `screenshots` and `resources` are optional.
+
+### Assets
+
+Cover image and screenshots are stored in the awesome-apps repository. Use a 3:2 aspect ratio and prefer PNG or JPEG.
+
+Follow this folder structure:
+
+- `webApps///cover.png`
+- `webApps///screenshots/1.png`
+
+If your repository hosts several apps, add the app name to the path, for example
+`webApps////cover.png`.
+
+Make sure the license of your assets allows us to keep them in the repository and to show them in the app store. If you
+are not sure about the license of an asset, do not add it.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/index.md b/versioned_docs/version-7.2/dev/web/extension-system/extension-types/index.md
deleted file mode 100644
index a6f04677d..000000000
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/index.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: 'Extension Types'
----
-
-This section is a guide about the different predefined extension types of OpenCloud Web. Please refer to the respective
-subpages to learn more about the individual extension types.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md
deleted file mode 100644
index 7d7e171e0..000000000
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md
+++ /dev/null
@@ -1,92 +0,0 @@
----
-title: 'Left sidebar menu item extensions'
-sidebar_position: 5
-id: left-sidebar-menu-item-extensions
----
-
-## Left sidebar menu item extension type
-
-One possible extension type is left sidebar menu items. Registered left sidebar menu items get rendered in the left sidebar, as long as there is more than one available.
-
-### Configuration
-
-To define a left sidebar menu item, you implement the SidebarNavExtension interface.
-It looks like this:
-
-```typescript
-interface SidebarNavExtension {
- id: string
- type: 'sidebarNav'
- extensionPointIds?: string[]
- navItem: AppNavigationItem // Please check the AppNavigationItem section below
- }
-}
-```
-
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
-
-#### AppNavigationItem
-
-The most important configuration options are:
-
-- `icon` - The icon to be displayed, can be picked from [Remix Icon](https://remixicon.com/)
-- `name` - The text to be displayed
-- `route` - The string/route to navigate to, if the nav item should be a `` (Mutually exclusive with `handler`)
-- `handler` - The action to perform upon click, if the nav item should be a `` (Mutually exclusive with `route`)
-
-Please check the [`AppNavigationItem` type](https://github.com/opencloud-eu/web/blob/f069ce44919cde5d112c68a519d433e015a4a011/packages/web-pkg/src/apps/types.ts#L14) for a full list of configuration options.
-
-### Example
-
-The following example shows an extension that adds a left sidebar nav item inside the files app, linking to a custom page. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) package.
-
-```typescript
-export const useCustomPageExtension = () => {
- const { $gettext } = useGettext();
-
- const extension = computed(() => ({
- id: 'com.github.opencloud-eu.web.files.left-nav.custom-page',
- extensionPointIds: ['app.files.navItems'],
- type: 'sidebarNav',
- action: {
- name: $gettext('Custom page'),
- icon: 'world',
- priority: 100,
- isActive: () => true,
- isVisible: () => true,
- route: {
- path: '/files/custom-page'
- },
- activeFor: [{ path: '/files/custom-page' }]
- }
- }));
-
- return { extension };
-};
-```
-
-The extension could then be registered in any app like so:
-
-```typescript
-export default defineWebApplication({
- setup() {
- const { extension } = useCustomPageExtension();
-
- return {
- appInfo: {
- name: $gettext('Custom page app'),
- id: 'custom-page-app'
- },
- routes: {
- path: '/files/custom-page',
- name: 'files-custom-page',
- component: CustomPageComponent,
- meta: {
- title: $gettext('Custom Page')
- }
- },
- extensions: computed(() => [unref(extension)])
- };
- }
-});
-```
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extensions/_category_.json b/versioned_docs/version-7.2/dev/web/extension-system/extensions/_category_.json
new file mode 100644
index 000000000..e9387689a
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Extensions",
+ "position": 4
+}
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-points.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-points.md
new file mode 100644
index 000000000..db2840062
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-points.md
@@ -0,0 +1,201 @@
+---
+title: 'Extension Points'
+sidebar_position: 1
+---
+
+Extension points are standardized places where extensions are used. If you build an extension that has the
+[type](./extension-types/) of an extension point, and lists the id of that extension point in its `extensionPointIds`,
+your extension gets used there without any further wiring.
+
+The lists below show the extension points that the OpenCloud Web runtime and the built-in apps provide. The `Multiple`
+column tells you if the extension point renders all matching extensions or only a single one. Your app can also
+[define its own extension points](#defining-your-own-extension-points), so that other apps can extend it.
+
+## Dynamic extension points
+
+Dynamic extension points are specific to each app. `${appId}` is the `id` you define in the `appInfo` of your app,
+for example `files` or `admin-settings`. Use these ids to add something to the user interface of a specific app,
+including apps you do not own (e.g. add a nav item to the sidebar of the files app).
+
+| Extension point id | Extension type | Multiple | Description |
+| ------------------------------------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------- |
+| `app.${appId}.navItems` | `sidebarNav` | yes | Navigation items in the left sidebar. |
+| `app.${appId}.sidebar-nav.main` | `customComponent` | yes | Main area of the left sidebar, below the nav items. |
+| `app.${appId}.sidebar-nav.bottom` | `customComponent` | yes | Bottom area of the left sidebar, above the version info. |
+| `app.${appId}.floating-action-button` | `floatingActionButton` | no | Primary action button. Rendered in the left sidebar on desktop and as a floating action button on mobile. |
+
+## Runtime
+
+| Extension point id | Extension type | Multiple | Description |
+| --------------------------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `app.runtime.header.left` | `customComponent` | yes | Left area of the global top bar. |
+| `app.runtime.header.center` | `customComponent` | yes | Center area of the global top bar. |
+| `app.runtime.header.right` | `customComponent` | yes | Right area of the global top bar. |
+| `app.runtime.header.app-menu` | `appMenuItem` | yes | Application switcher menu in the top left. |
+| `app.runtime.global-progress-bar` | `customComponent` | no | Progress bar for the global loading state. The user can pick one of the registered extensions on the preferences page. |
+| `app.runtime.preferences.panels` | `accountExtension` | yes | Panels on the preferences page, reachable via the top right user menu. |
+
+## Files app
+
+### Actions
+
+| Extension point id | Extension type | Multiple | Description |
+| ------------------------------------- | -------------- | -------- | ---------------------------------------------- |
+| `global.files.context-actions` | `action` | yes | Right click context menu of a resource. |
+| `global.files.batch-actions` | `action` | yes | Batch actions in the app bar above file lists. |
+| `global.files.resource-table-actions` | `action` | yes | Inline actions in a row of the resource table. |
+| `app.files.sidebar.actions` | `action` | yes | Actions panel of the right sidebar. |
+| `app.files.upload-menu` | `action` | yes | Upload menu. |
+| `app.files.quick-actions` | `action` | yes | Quick actions in a row of the resource table. |
+| `app.files.trash-quick-actions` | `action` | yes | Quick actions in a row of the trash overview. |
+
+### Right sidebar
+
+| Extension point id | Extension type | Multiple | Description |
+| --------------------------------------------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
+| `global.files.sidebar` | `sidebarPanel` | yes | Panels of the right sidebar. Used in any file context, meaning the files app as well as viewer and editor apps. |
+| `app.files.sidebar.file-details.table` | `customComponent` | no | Details table of a file. `space` and `resource` can be retrieved via injection context. |
+| `app.files.sidebar.space-details.table` | `customComponent` | no | Details table of a space. `space` and `resource` can be retrieved via injection context. |
+| `app.files.sidebar.shares-panel.shared-with.top` | `customComponent` | no | Top section of the people list in the shares panel. `space` and `resource` can be retrieved via injection context. |
+| `app.files.sidebar.shares-panel.shared-with.bottom` | `customComponent` | no | Bottom section of the people list in the shares panel. `space` and `resource` can be retrieved via injection context. |
+
+### Folder views
+
+Folder views define how the content of a page is presented, for example as a table or as a tile grid. Each page has its
+own extension point.
+
+| Extension point id | Extension type | Multiple | Description |
+| ------------------------------------------- | -------------- | -------- | ------------------------ |
+| `app.files.folder-views.folder` | `folderView` | no | Regular folders. |
+| `app.files.folder-views.project-spaces` | `folderView` | no | Project spaces overview. |
+| `app.files.folder-views.favorites` | `folderView` | no | Favorites page. |
+| `app.files.folder-views.trash` | `folderView` | no | Trash of a single space. |
+| `app.files.folder-views.trash-overview` | `folderView` | no | Trash overview. |
+| `app.files.folder-views.shared-with-me` | `folderView` | no | Shared with me page. |
+| `app.files.folder-views.shared-with-others` | `folderView` | no | Shared with others page. |
+| `app.files.folder-views.shared-via-link` | `folderView` | no | Shared via link page. |
+| `app.files.folder-views.search` | `folderView` | no | Search results page. |
+
+### Other
+
+| Extension point id | Extension type | Multiple | Description |
+| ---------------------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------ |
+| `app.files.floating-action-button` | `floatingActionButton` | no | Primary action button of the files app. Instance of the dynamic extension point described above. |
+
+## Other apps
+
+| Extension point id | Extension type | Multiple | Description |
+| ----------------------------- | -------------- | -------- | ---------------------------------------------------------- |
+| `app.search.provider` | `search` | yes | Search engines for the search input in the global top bar. |
+| `app.preview.toolbar-actions` | `action` | yes | Toolbar of the preview app. |
+
+## Defining your own extension points
+
+Define an extension point wherever your app has a place that other apps may fill. A good example is a toolbar or a
+panel that is useful beyond your own use case.
+
+### Declaring an extension point
+
+An extension point is a plain object of the `ExtensionPoint` type. Keep it in its own file, for example
+`src/extensionPoints.ts`, so that you can use it in several places:
+
+```typescript title="src/extensionPoints.ts"
+import { ActionExtension, ExtensionPoint } from '@opencloud-eu/web-pkg';
+
+export const toolbarExtensionPoint: ExtensionPoint = {
+ id: 'app.my-app.toolbar',
+ extensionType: 'action',
+ multiple: true
+};
+```
+
+`ExtensionPoint` accepts the following keys:
+
+- `id` - the id of the extension point. Extensions list it in their `extensionPointIds`. Use the same dot-formatted
+ namespace as for extension ids, and put your app id in it.
+- `extensionType` - the [type](./extension-types/) of the extensions that this extension point accepts.
+- `multiple` - whether all matching extensions are used, or only a single one. Defaults to `false`.
+- `defaultExtensionId` - the extension that is used when `multiple` is `false` and the user did not pick one.
+- `userPreference` - makes the extension point configurable by users, see below.
+
+### Registering an extension point
+
+Return your extension points from the app definition. The key takes a `Ref`, so use a `computed`:
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+import { toolbarExtensionPoint } from './extensionPoints';
+
+export default defineWebApplication({
+ setup() {
+ return {
+ appInfo: {
+ name: 'My app',
+ id: 'my-app'
+ },
+ extensionPoints: computed(() => [toolbarExtensionPoint])
+ };
+ }
+});
+```
+
+Registration is not needed to query extensions. It makes your extension point known to the runtime, which is required
+for the preferences page, and it documents the extension point for other developers.
+
+### Rendering the extensions
+
+For the type `customComponent`, mount a `CustomComponentTarget` at the place where the extensions belong. It queries
+the registry and respects the user preference:
+
+```html title="src/App.vue"
+
+
+
+
+
+```
+
+For all other types, query the registry yourself and render the result the way your extension point needs it:
+
+```typescript
+import { ActionExtension, useExtensionRegistry } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+import { toolbarExtensionPoint } from './extensionPoints';
+
+const extensionRegistry = useExtensionRegistry();
+
+const actions = computed(() =>
+ extensionRegistry
+ .requestExtensions(toolbarExtensionPoint)
+ .map(({ action }) => action)
+);
+```
+
+### Letting users choose
+
+Add a `userPreference` to your extension point to let users pick one of the registered extensions. This only makes
+sense when `multiple` is `false`:
+
+```typescript title="src/extensionPoints.ts"
+import { ExtensionPoint, CustomComponentExtension } from '@opencloud-eu/web-pkg';
+import { useGettext } from 'vue3-gettext';
+
+const { $gettext } = useGettext();
+
+export const progressBarExtensionPoint: ExtensionPoint = {
+ id: 'app.my-app.progress-bar',
+ extensionType: 'customComponent',
+ userPreference: {
+ label: $gettext('Progress bar'),
+ description: $gettext('Choose how the loading state is shown.')
+ }
+};
+```
+
+The extension point then gets a dropdown on the preferences page, reachable via the top right user menu. The dropdown
+only appears when at least one extension is registered for the extension point. Each extension can provide its own
+`userPreference.optionLabel` as the label in that dropdown.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/_category_.json b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/_category_.json
similarity index 100%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/_category_.json
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/_category_.json
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/account-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/account-extensions.md
new file mode 100644
index 000000000..a25f38c4f
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/account-extensions.md
@@ -0,0 +1,46 @@
+---
+title: 'Account extensions'
+sidebar_position: 9
+id: account-extensions
+---
+
+## Extension Type AccountExtension
+
+Account extensions add a panel to the preferences page, which is reachable via the user menu in the top right. Each
+registered extension gets its own entry in the navigation of that page.
+
+### Configuration
+
+To define an account extension, you implement the `AccountExtension` interface. Here's what it looks like:
+
+```typescript
+interface AccountExtension {
+ id: string;
+ type: 'accountExtension';
+ extensionPointIds?: string[];
+ content: Slot | Component;
+ label: () => string;
+ icon: string;
+}
+```
+
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
+
+- `content` - The component to render inside the panel.
+- `label` - Returns the title of the panel. Wrap it in `$gettext` to make it translatable.
+- `icon` - The icon of the navigation entry, can be picked from [Remix Icon](https://remixicon.com/).
+
+### Example
+
+The following example is taken from the Web runtime. It adds the app tokens panel to the preferences page.
+
+```typescript title="src/extensions.ts"
+const extension: AccountExtension = {
+ id: 'com.github.opencloud-eu.web.runtime.preferences-panels.app-tokens',
+ type: 'accountExtension',
+ extensionPointIds: ['app.runtime.preferences.panels'],
+ label: () => $gettext('App Tokens'),
+ icon: 'key-2',
+ content: AppTokens
+};
+```
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/action-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/action-extensions.md
similarity index 86%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/action-extensions.md
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/action-extensions.md
index d726150fc..fca7b2080 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/action-extensions.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/action-extensions.md
@@ -21,7 +21,7 @@ interface ActionExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
#### Action
@@ -35,13 +35,13 @@ The most important configuration options are:
- `handler` - The action to perform upon click. The nav item will be a `` tag.
- `isVisible` - Determines whether the action is displayed to the user
-Please check the [`Action` type](https://github.com/opencloud-eu/web/blob/236c185540fc6758dc7bd84985c8834fa4145530/packages/web-pkg/src/composables/actions/types.ts#L6) for a full list of configuration options.
+Please check the [`Action` type](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/web-pkg/src/composables/actions/types.ts#L6) for a full list of configuration options.
### Example
-The following example shows how an action extension for downloading files could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/master/packages/web-pkg) package.
+The following example shows how an action extension for downloading files could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/stable-7.1/packages/web-pkg) package.
-```typescript
+```typescript title="src/extensions.ts"
export const useDownloadFilesExtension = () => {
const { $gettext } = useGettext();
@@ -73,7 +73,7 @@ export const useDownloadFilesExtension = () => {
The extension could then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useFileActionDownloadFiles();
diff --git a/docs/dev/web/extension-system/extension-types/app-menu-item-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/app-menu-item-extensions.md
similarity index 88%
rename from docs/dev/web/extension-system/extension-types/app-menu-item-extensions.md
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/app-menu-item-extensions.md
index fa2c6f2fd..152ba7ca8 100644
--- a/docs/dev/web/extension-system/extension-types/app-menu-item-extensions.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/app-menu-item-extensions.md
@@ -19,7 +19,7 @@ interface AppMenuItemExtension {
extensionPointIds?: string[];
label: () => string;
color?: string;
- handler?: () => void;
+ handler?: () => Promise | void;
icon?: string;
path?: string;
priority?: number;
@@ -27,7 +27,7 @@ interface AppMenuItemExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
A `handler` will result in a `` element. This is necessary when an action should be performed when clicking the menu item (e.g. opening a file editor).
@@ -41,9 +41,9 @@ At least one of these properties has to be provided when registering an extensio
## Example
-The following example shows how an app creates an extension that registers an app menu item, linking to an internal page. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) package.
+The following example shows how an app creates an extension that registers an app menu item, linking to an internal page. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/stable-7.1/packages/web-pkg) package.
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { $gettext } = useGettext();
diff --git a/docs/dev/web/extension-system/extension-types/custom-component-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/custom-component-extensions.md
similarity index 80%
rename from docs/dev/web/extension-system/extension-types/custom-component-extensions.md
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/custom-component-extensions.md
index d55be79a7..5ff4af3ee 100644
--- a/docs/dev/web/extension-system/extension-types/custom-component-extensions.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/custom-component-extensions.md
@@ -20,19 +20,20 @@ interface CustomComponentExtension {
type: 'customComponent';
extensionPointIds?: string[];
content: Slot | Component;
+ componentProps?: () => Record;
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
-The `content` property specifies a render function or a Component for the target extension point.
+The `content` property specifies a render function or a Component for the target extension point. Use `componentProps` to pass props to that component.
### Example
A simple example for a custom component extension could be a `NyanCat` progress bar component, being
targeted at the `global-progress-bar` extension point as render target.
-```typescript
+```typescript title="src/extensions.ts"
const extension = {
id: 'com.github.opencloud-eu.web.app.progress-bars.nyan-cat',
type: 'customComponent',
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/floating-action-button-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/floating-action-button-extensions.md
similarity index 79%
rename from versioned_docs/version-7.2/dev/web/extension-system/extension-types/floating-action-button-extensions.md
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/floating-action-button-extensions.md
index 8b6772897..d65c415bc 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/extension-types/floating-action-button-extensions.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/floating-action-button-extensions.md
@@ -9,7 +9,7 @@ id: floating-action-button-extensions
This extension type allows apps to register actions either displayed within the left sidebar (for desktop resolutions) or as a floating action button (for mobile resolutions). The extension point for this extension type is `app.${appId}.floating-action-button`.
:::warning
-You need to take care of the visibility of your floating action button extension via the `isActive` property, otherwise you might end up overwriting other extensions' action buttons. In most cases, it makes sense to only display the button when your app is currently active.
+You need to take care of the visibility of your floating action button extension via the `isVisible` property, otherwise you might end up overwriting other extensions' action buttons. In most cases, it makes sense to only display the button when your app is currently active.
:::
### Configuration
@@ -22,7 +22,7 @@ interface FloatingActionButtonExtension {
type: 'floatingActionButton';
extensionPointIds?: string[];
label: () => string;
- isActive: () => boolean;
+ isVisible?: () => boolean;
isDisabled?: () => boolean;
color?: string;
icon?: string;
@@ -32,11 +32,11 @@ interface FloatingActionButtonExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
The `mode` property determines whether the floating action button triggers a handler function or renders a `dropComponent` when clicked. If mode returns `handler`, the click handler function is executed, if it returns `drop`, the specified `dropComponent` is shown instead.
-`isDisabled` controls the disabled state of the button whereas `isActive` determines if the button is showing at all.
+`isDisabled` controls the disabled state of the button whereas `isVisible` determines if the button is showing at all.
`icon` is an icon name string that can be picked from [Remix Icon](https://remixicon.com/).
@@ -44,9 +44,10 @@ The `mode` property determines whether the floating action button triggers a han
The following example shows how the files app is registering a floating action button extension for creating new files or folders. Note that the example assumes you're in a Vue injection context (e.g. within the `setup` method of your app's `defineWebApplication` call).
-```typescript
+```typescript title="src/extensions.ts"
import { useGettext } from 'vue3-gettext';
import CreateOrUploadMenu from './components/CreateOrUploadMenu.vue';
+import { markRaw } from 'vue';
import { useIsFilesAppActive, useResourcesStore } from '@opencloud-eu/web-pkg';
const { $gettext } = useGettext();
@@ -59,12 +60,13 @@ const extension = {
type: 'floatingActionButton',
icon: 'add',
label: () => $gettext('New'),
- isActive: () => {
+ isVisible: () => {
return unref(isFilesAppActive);
},
isDisabled: () => {
return !resourcesStore.currentFolder?.canUpload();
},
- dropComponent: CreateOrUploadMenu
+ mode: () => 'drop',
+ dropComponent: markRaw(CreateOrUploadMenu)
};
```
diff --git a/docs/dev/web/extension-system/extension-types/folder-view-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/folder-view-extensions.md
similarity index 83%
rename from docs/dev/web/extension-system/extension-types/folder-view-extensions.md
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/folder-view-extensions.md
index f18d81654..14d14ae6c 100644
--- a/docs/dev/web/extension-system/extension-types/folder-view-extensions.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/folder-view-extensions.md
@@ -21,7 +21,7 @@ interface FolderViewExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
#### FolderView
@@ -30,15 +30,14 @@ For the folderView object, you have the following configuration options:
- `name` - The name of the action (not displayed in the UI)
- `label` - The text to be displayed to the user when switching between different FolderView options
- `icon` - Object, expecting an icon `name` and a corresponding `IconFillType`, see [Remix Icon](https://remixicon.com/) for available options
-- `isScrollable` - Optional boolean, determines whether the user can scroll inside the component or it statically fills the viewport
- `component` - The Vue component to render the resources. It should expect a prop of type `Resource[]`
-- `componentAttrs` - Optional additional configuration for the component mentioned above
+- `componentAttrs` - Optional function returning additional props for the component mentioned above
### Example
-The following example shows how an extension for a custom folder view could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) package.
+The following example shows how an extension for a custom folder view could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/stable-7.1/packages/web-pkg) package.
-```typescript
+```typescript title="src/extensions.ts"
export const useCustomFolderViewExtension = () => {
const { $gettext } = useGettext();
@@ -63,7 +62,7 @@ export const useCustomFolderViewExtension = () => {
The extension could then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useCustomFolderViewExtension();
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/index.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/index.md
new file mode 100644
index 000000000..27b2e1b60
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/index.md
@@ -0,0 +1,23 @@
+---
+title: 'Extension Types'
+---
+
+For building an extension you can choose from the types predefined by the OpenCloud Web extension system. The full list
+is shown below. Please refer to the respective subpages to learn more about the individual extension types.
+
+| Extension type | `type` | Description |
+| ---------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [`ActionExtension`](./action-extensions) | `action` | Registers `Action` items that get shown in various places, for example context menus or batch actions. Most commonly used for file and folder actions like copy, rename or delete. |
+| [`AppMenuItemExtension`](./app-menu-item-extensions) | `appMenuItem` | Registers links to internal or external pages in the application switcher menu. |
+| [`CustomComponentExtension`](./custom-component-extensions) | `customComponent` | Registers a custom component for a render target. |
+| [`FolderViewExtension`](./folder-view-extensions) | `folderView` | Registers additional ways of displaying the content of a folder, meaning resources like spaces, folders or files. |
+| [`SidebarNavExtension`](./left-sidebar-menu-item-extensions) | `sidebarNav` | Registers additional navigation items for the left sidebar. These can be scoped to specific apps, and enabled or disabled programmatically. |
+| [`SidebarPanelExtension`](./right-sidebar-panel-extensions) | `sidebarPanel` | Registers panels for the right sidebar. |
+| [`SearchExtension`](./search-extensions) | `search` | Registers additional search providers. |
+| [`FloatingActionButtonExtension`](./floating-action-button-extensions) | `floatingActionButton` | Registers one or multiple primary actions. Displayed in the left sidebar on desktop resolutions and as a floating action button on mobile resolutions. |
+| [`AccountExtension`](./account-extensions) | `accountExtension` | Registers a panel on the preferences page. |
+
+You're free to introduce your own extension types within your application code and use the extension registry to query
+the available ones. However, if you have the impression that an important extension type is missing and would be
+beneficial for the platform, please reach out to us by opening a
+[GitHub issue](https://github.com/opencloud-eu/web/issues/new/choose).
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/left-sidebar-menu-item-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/left-sidebar-menu-item-extensions.md
new file mode 100644
index 000000000..6d96c8e2d
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/left-sidebar-menu-item-extensions.md
@@ -0,0 +1,94 @@
+---
+title: 'Left sidebar menu item extensions'
+sidebar_position: 5
+id: left-sidebar-menu-item-extensions
+---
+
+## Left sidebar menu item extension type
+
+One possible extension type is left sidebar menu items. Registered left sidebar menu items get rendered in the left sidebar, as long as there is more than one available.
+
+### Configuration
+
+To define a left sidebar menu item, you implement the `SidebarNavExtension` interface. It looks like this:
+
+```typescript
+interface SidebarNavExtension {
+ id: string;
+ type: 'sidebarNav';
+ extensionPointIds?: string[];
+ navItem: AppNavigationItem; // Please check the AppNavigationItem section below
+}
+```
+
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
+
+Each app has its own nav items extension point with the id `app.${appId}.navItems`. Use it in `extensionPointIds` to
+place your nav item in the left sidebar of that app.
+
+#### AppNavigationItem
+
+The most important configuration options are:
+
+- `icon` - The icon to be displayed, can be picked from [Remix Icon](https://remixicon.com/)
+- `name` - The text to be displayed
+- `route` - The string/route to navigate to, if the nav item should be a `` (Mutually exclusive with `handler`)
+- `handler` - The action to perform upon click, if the nav item should be a `` (Mutually exclusive with `route`)
+
+Please check the [`AppNavigationItem` type](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/web-pkg/src/apps/types.ts) for a full list of configuration options.
+
+### Example
+
+The following example shows an app that adds a nav item to the left sidebar of the admin settings app. The nav item
+links to a view of the app itself.
+
+```typescript title="src/index.ts"
+import { defineWebApplication, SidebarNavExtension } from '@opencloud-eu/web-pkg';
+import { useGettext } from 'vue3-gettext';
+import { computed } from 'vue';
+import App from './App.vue';
+
+export default defineWebApplication({
+ setup() {
+ const { $gettext } = useGettext();
+ const appId = 'admin-settings/office';
+
+ const extensions = computed(() => [
+ {
+ id: 'com.github.my-org.my-app.admin-settings.left-nav.office',
+ type: 'sidebarNav',
+ extensionPointIds: ['app.admin-settings.navItems'],
+ navItem: {
+ isVisible: () => true,
+ name: $gettext('Office'),
+ icon: 'attachment',
+ route: {
+ path: `/${appId}`
+ }
+ }
+ }
+ ]);
+
+ const routes = [
+ {
+ path: '/',
+ name: 'office',
+ component: App,
+ meta: {
+ title: $gettext('Office Settings'),
+ authContext: 'user'
+ }
+ }
+ ];
+
+ return {
+ appInfo: {
+ name: $gettext('Office Settings'),
+ id: appId
+ },
+ routes,
+ extensions
+ };
+ }
+});
+```
diff --git a/docs/dev/web/extension-system/extension-types/right-sidebar-panel-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/right-sidebar-panel-extensions.md
similarity index 92%
rename from docs/dev/web/extension-system/extension-types/right-sidebar-panel-extensions.md
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/right-sidebar-panel-extensions.md
index 91baa35fb..3b5d97320 100644
--- a/docs/dev/web/extension-system/extension-types/right-sidebar-panel-extensions.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/right-sidebar-panel-extensions.md
@@ -28,7 +28,7 @@ interface SidebarPanelExtension
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
The `panel` object configures the actual sidebar panel. It consists of different properties and functions, where all the functions get called with a
`SideBarPanelContext` entity from the integrating extension points.
@@ -81,9 +81,9 @@ will automatically become available in all environments that display the `FileSi
## Example
-The following example shows how a sidebar panel for displaying exif data for a resource could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) and the [web-client](https://github.com/opencloud-eu/web/tree/main/packages/web-client) packages.
+The following example shows how a sidebar panel for displaying exif data for a resource could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/stable-7.1/packages/web-pkg) and the [web-client](https://github.com/opencloud-eu/web/tree/stable-7.1/packages/web-client) packages.
-```typescript
+```typescript title="src/extensions.ts"
export const useExifDataPanelExtension = () => {
const { $gettext } = useGettext();
@@ -113,7 +113,7 @@ export const useExifDataPanelExtension = () => {
The extension can then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useExifDataPanelExtension();
diff --git a/docs/dev/web/extension-system/extension-types/search-extensions.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/search-extensions.md
similarity index 89%
rename from docs/dev/web/extension-system/extension-types/search-extensions.md
rename to versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/search-extensions.md
index aa59eb46b..ce46c8baa 100644
--- a/docs/dev/web/extension-system/extension-types/search-extensions.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/extension-types/search-extensions.md
@@ -28,7 +28,7 @@ interface SearchExtension {
}
```
-For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs.
+For `id`, `type`, and `extensionPointIds`, please see [base configuration section](../index.md#base-configuration) in the extensions docs.
The `searchProvider` object configures the actual provider. It consist of the following:
@@ -53,9 +53,9 @@ The previewSearch object extends the listSearch with one additional attribute:
## Example
-The following example shows how a search extension that queries a Solr search engine could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/main/packages/web-pkg) and the [web-client](https://github.com/opencloud-eu/web/tree/main/packages/web-client) packages.
+The following example shows how a search extension that queries a Solr search engine could look like. Note that the extension is wrapped inside a Vue composable so it can easily be reused. All helper types and composables are being provided via the [web-pkg](https://github.com/opencloud-eu/web/tree/stable-7.1/packages/web-pkg) and the [web-client](https://github.com/opencloud-eu/web/tree/stable-7.1/packages/web-client) packages.
-```typescript
+```typescript title="src/extensions.ts"
export const useSolrSearchExtension = () => {
const { $gettext } = useGettext();
@@ -87,7 +87,7 @@ export const useSolrSearchExtension = () => {
The search component for the preview search container may look like this:
-```html
+```html title="src/components/SolarSearchComponent.vue"
@@ -105,7 +105,7 @@ The search component for the preview search container may look like this:
The extension can then be registered in any app like so:
-```typescript
+```typescript title="src/index.ts"
export default defineWebApplication({
setup() {
const { extension } = useSolrSearchExtension();
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/extensions/index.md b/versioned_docs/version-7.2/dev/web/extension-system/extensions/index.md
new file mode 100644
index 000000000..d8bff39f5
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/extensions/index.md
@@ -0,0 +1,133 @@
+---
+title: 'Extensions'
+---
+
+Extensions are small, scoped pieces of functionality that can be added to the OpenCloud Web interface. They typically
+hook into specific [extension points](./extension-points) and always need to be shipped by an app.
+
+Every extension has a [type](./extension-types/), which defines what the extension does and which data it provides.
+The extension point it registers on defines where it appears in OpenCloud Web.
+
+## Extension registry
+
+The OpenCloud Web runtime provides a globally available extension registry. It is used to both register and query
+extensions. All extensions that are made available via an app get registered in the extension registry automatically.
+
+## Base configuration
+
+Any extension is required to define at least an `id` and a `type` in order to fulfill the generic `Extension` interface.
+
+The `id` is supposed to be unique throughout the OpenCloud Web ecosystem. In order to keep `id`s readable for humans we
+didn't want to enforce uniqueness through e.g. uuids. Instead, we chose to use dot-formatted namespaces like e.g.
+`com.github.opencloud-eu.web.files.search`. We'd like to encourage you to follow the same format for your own extensions.
+
+For the `type` you can choose from the [predefined extension types](./extension-types/) or define a custom one.
+
+In addition, you can pass optional `extensionPointIds` to determine where the extension will appear. You can find all
+predefined ids in the [extension points docs](./extension-points).
+
+## Registering extensions
+
+### Via the app definition
+
+The `extensions` key of an app definition takes a `Ref`, so use a `computed`. The runtime registers its
+content when your app is loaded:
+
+```typescript title="src/index.ts"
+import { defineWebApplication, Extension } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+
+export default defineWebApplication({
+ setup() {
+ const extensions = computed(() => [
+ {
+ id: 'com.github.my-org.my-app.my-action',
+ type: 'action',
+ extensionPointIds: ['global.files.context-actions'],
+ action: {
+ // See the action extension docs
+ }
+ }
+ ]);
+
+ return {
+ appInfo: {
+ name: 'My app',
+ id: 'my-app'
+ },
+ extensions
+ };
+ }
+});
+```
+
+Because the value is a ref, the list is reactive. You can hide or show an extension based on the app config, the
+capabilities of the server, or the permissions of the user:
+
+```typescript
+const extensions = computed(() => {
+ if (!unref(isFeatureAvailable)) {
+ return [];
+ }
+ return [myExtension];
+});
+```
+
+### At runtime
+
+You can also register extensions from any place that has access to the injection context, for example a component or a
+composable:
+
+```typescript
+import { useExtensionRegistry } from '@opencloud-eu/web-pkg';
+
+const extensionRegistry = useExtensionRegistry();
+
+extensionRegistry.registerExtensions(computed(() => [myExtension]));
+```
+
+Remove them again via `unregisterExtensions`, which takes the ids:
+
+```typescript
+extensionRegistry.unregisterExtensions([myExtension.id]);
+```
+
+Prefer the app definition. Use the runtime API only when the extensions are not known at that point, for example because
+they depend on data that you load first.
+
+:::note
+Administrators can switch off single extensions via the `options.disabledExtensions` key of the OpenCloud Web config. A
+disabled extension stays registered, but the registry does not return it.
+:::
+
+## Querying extensions
+
+Use `requestExtensions` to get all extensions of an extension point. The registry returns only the extensions whose
+`type` matches the `extensionType` of the extension point, and whose `extensionPointIds` allow this extension point:
+
+```typescript
+import { ActionExtension, useExtensionRegistry } from '@opencloud-eu/web-pkg';
+import { computed } from 'vue';
+
+const extensionRegistry = useExtensionRegistry();
+
+const actions = computed(() =>
+ extensionRegistry.requestExtensions(myExtensionPoint).map(({ action }) => action)
+);
+```
+
+To learn how to define `myExtensionPoint`, please refer to the
+[extension points docs](./extension-points#defining-your-own-extension-points).
+
+## User preferences
+
+Extension points can let users choose between the registered extensions. The extension point then gets a dropdown on
+the preferences page, reachable via the top right user menu. Your extension provides the label for that dropdown via
+`userPreference.optionLabel`. For the extension point side, please refer to the
+[extension points docs](./extension-points#letting-users-choose).
+
+## Creating an extension
+
+Please check out the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) for a boilerplate
+app that also includes an extension. In addition to that, the [extension types docs](./extension-types/) provide
+instructions and examples on how to implement the different extension types.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/getting-started.md b/versioned_docs/version-7.2/dev/web/extension-system/getting-started.md
new file mode 100644
index 000000000..1c5d60e9e
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/getting-started.md
@@ -0,0 +1,178 @@
+---
+title: 'Getting started'
+sidebar_position: 1
+---
+
+This guide shows how to set up and run an OpenCloud Web app.
+
+## Prerequisites
+
+- git
+- docker and docker compose
+- node
+- [pnpm](https://pnpm.io/installation), ideally installed via `corepack`
+
+If you don't use Docker Desktop, add `127.0.0.1 host.docker.internal` to your `/etc/hosts` file. Otherwise
+`host.docker.internal` cannot be resolved.
+
+## Project setup
+
+The fastest way to start is the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton). It
+contains a working app, a docker compose setup with an OpenCloud server, and a unit test setup.
+
+```bash
+git clone https://github.com/opencloud-eu/web-app-skeleton.git my-app
+cd my-app
+pnpm install
+```
+
+Rename the app afterwards. The name `skeleton` appears in `package.json`, `vite.config.ts`, `src/index.ts`,
+and `tests/unit/App.spec.ts`.
+
+:::note
+The skeleton follows the latest OpenCloud release. For OpenCloud 7.2, pin the `@opencloud-eu/*` dependencies in
+`package.json` to `^7.0.0` and set `OC_IMAGE` in `docker-compose.yml` to a 7.x server image.
+:::
+
+## Running your app
+
+There are two ways to run your app against a local OpenCloud instance.
+
+### Watch build
+
+This mode fully builds your app and writes it into the `dist` folder, which is then served by the OpenCloud server.
+
+1. Start a watch build. It writes your app into the `dist` folder on every change.
+
+ ```bash
+ pnpm build:w
+ ```
+
+2. Start the OpenCloud server. In the skeleton repository, the `dist` folder is already mounted into the container, and
+ `WEB_ASSET_APPS_PATH` points to the mount target.
+
+ ```bash
+ docker compose up
+ ```
+
+3. Open [https://host.docker.internal:9200](https://host.docker.internal:9200) and log in as `admin` with the password
+ `admin`. Your app is loaded automatically.
+
+Changes are picked up by the watch build, but you need to reload the page to see them.
+
+### Module federation with hot reload
+
+In this mode your app is served by its own Vite dev server and loaded into a running OpenCloud Web dev server as a
+federated module. You get instant hot reload, but you need a local checkout of the
+[web repository](https://github.com/opencloud-eu/web).
+
+1. Start the OpenCloud Web dev server via `pnpm vite` in your `web` checkout, as described in the
+ [tooling docs](../development/tooling#using-instant-hot-reload-via-vite). It listens on
+ [https://host.docker.internal:9201](https://host.docker.internal:9201).
+
+2. Start the dev server of your app:
+
+ ```bash
+ pnpm vite
+ ```
+
+ It listens on port `9210` by default. Change it via the `server.port` option in your Vite config.
+
+3. Open [https://host.docker.internal:9210](https://host.docker.internal:9210) and accept the self-signed certificate
+ (adjust the port if you changed it in your Vite config).
+
+4. Open [https://host.docker.internal:9201](https://host.docker.internal:9201).
+
+The extension-sdk registers your app with the OpenCloud Web dev server every few seconds, so the registration survives a
+restart of either server.
+
+## The app definition
+
+The `src/index.ts` file acts as the entrypoint of the app. This file has to export an app definition created via
+`defineWebApplication`:
+
+```typescript title="src/index.ts"
+import { defineWebApplication } from '@opencloud-eu/web-pkg'
+import { computed } from 'vue';
+import { useGettext } from 'vue3-gettext';
+
+// Needs to be unique within all installed applications in any OpenCloud
+// web instance. Should be short, unique and expressive as it is used as
+// prefix on all routes within your application.
+const appId = 'your-app'
+
+export default defineWebApplication({
+ setup({ applicationConfig }) {
+ // Here, you have access to the full injection context.
+ const { $gettext } = useGettext();
+
+ return {
+ appInfo: {
+ name: $gettext('Your application name'),
+ id: appId,
+ icon: 'aliens' // See https://remixicon.com
+ },
+ navItems: [ ... ],
+ routes: [ ... ],
+ extensions: computed( () => [ ... ]),
+ extensionPoints: computed( () => [ ... ]),
+ translations: { ... }
+ }
+ }
+})
+```
+
+`defineWebApplication` accepts the following keys:
+
+- `appInfo` - the application metadata. It makes the application available via the app switcher and the app registry.
+- `navItems` - the statically defined navigation items for the left sidebar. They only get rendered when more than 1
+ navigation item exists at runtime. Additional dynamic navigation items can be registered via the extension registry.
+- `routes` - the routes to the different views of your application. They may be referenced within the `navItems`.
+ Authentication requirements can be defined per item.
+- `extensions` - the extensions to be registered in the extension registry. For details, please refer to the
+ [extensions docs](./extensions/).
+- `extensionPoints` - the extension points to be registered in the extension registry. For details, please refer to the
+ [extension points docs](./extensions/extension-points).
+- `translations` - the translations of your application. For details, please refer to the
+ [translations docs](./advanced-topics/translations).
+
+## Vite configuration
+
+Apps are built with [Vite](https://vite.dev/). The `@opencloud-eu/extension-sdk` package provides a ready to use Vite
+config, so your `vite.config.ts` stays short:
+
+```typescript title="vite.config.ts"
+import { defineConfig } from '@opencloud-eu/extension-sdk';
+
+export default defineConfig({
+ name: 'my-app'
+});
+```
+
+`defineConfig` accepts any [Vite option](https://vite.dev/config/), plus the following:
+
+- `name` - The name of your app. Defaults to the `name` field of your `package.json`.
+- `opencloudWebHostUrl` - The URL of the OpenCloud Web dev server. Defaults to `https://host.docker.internal:9201`.
+
+The config sets up Vue, [Tailwind CSS](./advanced-topics/styling#tailwind-css), module federation and the generation of
+`manifest.json`. It also declares the modules that the OpenCloud Web runtime shares with your app, such as `vue`, `pinia`,
+`@opencloud-eu/web-pkg` and `@opencloud-eu/web-client`. These modules must not be bundled into your app.
+
+The following environment variables are supported:
+
+| Variable | Description |
+| ------------------------------ | -------------------------------------------------------------------- |
+| `OPENCLOUD_WEB_HOST_URL` | URL of the OpenCloud Web dev server. Same as `opencloudWebHostUrl`. |
+| `OPENCLOUD_EXTENSION_DIST_DIR` | Output directory of the build. Defaults to `dist`. |
+| `OPENCLOUD_CERTS_DIR` | Directory with a `server.key` and a `server.crt` for the dev server. |
+
+## What's next?
+
+- [Build and publish](./build-and-publish) your app, so that users can install it.
+- Register [extensions](./extensions/) to add functionality to existing places of the OpenCloud Web user interface.
+- Make your app [configurable](./advanced-topics/configuration) for administrators.
+- Follow the platform look with the [styling docs](./advanced-topics/styling).
+- Add [translations](./advanced-topics/translations) to your app.
+- Write [tests](./advanced-topics/testing) for your app.
+- Build a [viewer or editor app](./viewer-editor-apps) for specific file types.
+- Reuse our [helpful packages](./packages) instead of writing your own utilities.
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/index.md b/versioned_docs/version-7.2/dev/web/extension-system/index.md
index 5962d994a..8e8a1a772 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/index.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/index.md
@@ -1,188 +1,33 @@
---
-title: 'Extension system'
+title: 'Extension System'
---
-## Concepts and Building Blocks
+OpenCloud Web can be extended with **apps**. An app is an artifact that gets installed in an OpenCloud instance.
+It is the main building block of the extension system: everything you add to OpenCloud Web is delivered as an app.
-OpenCloud Web can be extended through various entry points with custom **apps** and **extensions**.
+An app can do two things, and both of them are optional:
-### Distinction between Apps and Extensions
+1. It can take over the full app viewport, meaning everything below the top bar. There you can render any custom
+ application code, define views with routes, add navigation items to the left sidebar, and more.
+2. It can register [extensions](./extensions/). Extensions are small, focused pieces of functionality that get mounted
+ into predefined places of the OpenCloud Web user interface, for example a file action or a panel in the right sidebar.
-An Application in the context of OpenCloud Web is an artifact which can be installed in an OpenCloud instance.
-It serves two main purposes:
+This means an app can be a file editor without any extensions, a pure extension host without any custom views, or a
+combination of both.
-1. It makes the full app viewport (everything below the top bar) available to the application developer for any custom
- application code. This includes the ability to define views with routes, navigation items for the left sidebar, and more.
-2. Through the `extensions` key in the application interface you can register extensions of any extension type. Those extensions
- are then available in standardized extension points. Additionally, they can be queried from the extension registry for
- your own purposes.
+## Creating an app
-Both parts are optional. This means that an application can be a file editor without any custom extensions, or even contain
-no custom application code at all and only host extensions to be registered in the extension registry, or a combination of both.
+The [getting started guide](./getting-started) takes you from an empty folder to a running app. It is the entrypoint
+for app development.
-### Examples
+## Examples
-You can find open source examples for apps and extensions in our [curated list of OpenCloud apps and extensions](https://github.com/opencloud-eu/awesome-apps).
-Feel free to contribute or just be inspired for your own apps or extensions.
+You can find open source examples for apps and extensions in our
+[curated list of OpenCloud apps and extensions](https://github.com/opencloud-eu/awesome-apps).
+Feel free to contribute or just be inspired for your own apps and extensions.
-### Apps
+## Installing an app
-Apps define a `src/index.ts` file which acts as an entrypoint. Below is the most basic example of its content:
-
-```typescript
-import {
- AppWrapperRoute,
- ApplicationFileExtension,
- defineWebApplication
-} from '@opencloud-eu/web-pkg'
-
-
-export default defineWebApplication({
- setup({ applicationConfig }) {
- // Here, you have access to the full injection context, meaning you can use all composables that we provide via web-pkg
-
- // Needs to be unique within all installed applications in any OpenCloud web instance
- // Should be short, unique and expressive as it is used as prefix on all routes within your application
- const appId = 'your-extension'
-
- // See extensions section below
- const extensions = [
- ...
- ]
-
- // See details below
- const navItems = [
- ...
- ]
-
- // See details below
- const routes = [
- ...
- ]
-
- return {
- appInfo: {
- name: $gettext('Your application name'),
- id: appId,
- icon: 'aliens', // See https://remixicon.com/ for available options
- },
- extensions,
- navItems,
- routes
- }
- }
-})
-```
-
-By defining an application via `defineWebApplication` you can provide the following:
-
-- `appInfo` - the application metadata, which is used to make the application available via the app switcher and the app registry.
-- `navItems` - the statically defined navigation items for the left sidebar. Only gets rendered when more than 1 navigation item exists at runtime.
- Additional dynamic navigation items can be registered via the extension registry.
-- `routes` - the routes to the different views of your application. May be referenced within the `navItems`. Authentication requirements can be defined per item.
-- `extensions` - the extensions to be registered in the extension registry. For more details see the [Extensions](#extensions) section below.
-
-#### Creating a new app
-
-Please check out the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) for a boilerplate application to get started with your own application. In addition to that, if you want to learn how to implement an app for viewing and editing specific file types, please consult the [relevant documentation](./viewer-editor-apps) for detailed instructions and guidance.
-
-To learn how to integrate an app into OpenCloud Web, please refer to the [Web Application admin docs](../../../admin/configuration/web-applications).
-
-### Extensions
-
-In contrast to applications, extensions usually have a rather small scope and dedicated functionality.
-
-#### Extension Registry
-
-The globally available extension registry provided by the OpenCloud Web runtime can be used to both register and query extensions. All extensions
-which are being made available via an `app` get registered in the extension registry automatically. In your custom application code you can
-then query any of the available extensions by providing an `extensionPoint` entity. Throughout the OpenCloud Web platform
-and most prominently also in the `files` app we have defined some extension points which automatically use certain extensions, see the
-`Extension Points` section below.
-
-#### Extension Types
-
-For building an extension you can choose from the types predefined by the OpenCloud Web extension system. See the full list of available extension types below.
-
-1. `ActionExtension` (type `action`) - An extension that can register `Action` items which then get shown in various places (e.g. context menus, batch actions), depending on the
- extension points referenced in the extension respectively. Most commonly used for file and folder actions (e.g. copy, rename, delete, etc.). For details, please refer to the [action docs](./extension-types/action-extensions)
-2. `SearchExtension` (type `search`) - An extension that can register additional search providers. For details, please refer to the [search docs](./extension-types/search-extensions).
-3. `SidebarNavExtension` (type `sidebarNav`) - An extension that can register additional navigation items for the left sidebar. These can be scoped to specific apps, and programmatically enabled/disabled.
- For details, please refer to the [sidebar nav docs](./extension-types/left-sidebar-menu-item-extensions).
-4. `SidebarPanelExtension`, (type `sidebarPanel`) - An extension that can register panels for the right sidebar. For details, please refer to the [sidebar panel docs](./extension-types/right-sidebar-panel-extensions).
-5. `FolderViewExtension` (type `folderView`) - An extension that can register additional ways of displaying the content of a folder (resources like spaces, folders or files) to the user.
- For details, please refer to the [folder view docs](./extension-types/folder-view-extensions).
-6. `CustomComponentExtension` (type `customComponent`) - An extension that can register a custom component for a render target. For details, please refer to the
- [custom component docs](./extension-types/custom-component-extensions)
-7. `FloatingActionButtonExtension` (type `floatingActionButton`) - An extension that can register one or multiple actions, either displayed within the left sidebar (for desktop resolutions) or as a floating action button (for mobile resolutions). For details, please refer to the
- [floating action button docs](./extension-types/floating-action-button-extensions).
-
-You're free to introduce your own extension types within your application code and use the extension registry to query the available ones. However, if you have the impression
-that an important extension type is missing and would be beneficial for the platform, please reach out to us by opening a [GitHub issue](https://github.com/opencloud-eu/web/issues/new/choose).
-
-#### Extension Base Configuration
-
-Any extension is required to define at least an `id` and a `type` in order to fulfill the generic `Extension` interface.
-
-The `id` is supposed to be unique throughout the OpenCloud Web ecosystem. In order to keep `id`s readable for humans we didn't want to enforce uniqueness through e.g. uuids.
-Instead, we chose to use dot-formatted namespaces like e.g. `com.github.opencloud-eu.web.files.search`. We'd like to encourage you to follow the same format for your own extensions.
-
-For the `type` you can choose from the ones listed above or define a custom one.
-
-In addition, you can also pass optional `extensionPointIds` to further limit the usage of an extension. With the right click context menu and the batch actions being
-two different extension points, this could mean that a file action extension is only allowed in the context menu, but not in the batch actions.
-You can find predefined extension point ids in the extension points section below.
-
-#### Extension Points
-
-There are standardized components and places where extensions are being used automatically. The following ones are currently provided by the OpenCloud Web runtime or
-the `files` app. If you decide to develop an extension which fulfills the type and registers itself for the extensionPointId of the respective extension point,
-your extension will be used automatically.
-
-1. Left Sidebar for Navigation.
- 1. Floating Action Button (FAB). ExtensionPointId `app.${appName}.floating-action-button` (dynamically created for each app). Mounts extensions of type `floatingActionButton`.
- 2. Additional nav items. ExtensionPointId `app.${appName}.navItems` (dynamically created for each app). Mounts extensions of type `sidebarNav`.
- 3. Sidebar main area below nav items. ExtensionPointId `app.${appName}.sidebar-nav.main` (dynamically created for each app). Mounts extensions of type `customComponent`.
- 4. Sidebar bottom area above version info. ExtensionPointId `app.${appName}.sidebar-nav.bottom` (dynamically created for each app). Mounts extensions of type `customComponent`.
-2. Global top bar
- 1. Center area. ExtensionPointId `app.runtime.header.center`. Mounts extensions of type `customComponent`.
- 2. Left area. ExtensionPointId `app.runtime.header.left`. Mounts extensions of type `customComponent`.
- 3. Right area. ExtensionPointId `app.runtime.header.right`. Mounts extensions of type `customComponent`.
- 4. Progress bar for the global loading state. ExtensionPointId `app.runtime.global-progress-bar`. Mounts a single extension of type `customComponent`. If multiple exist, the user can choose via the account page.
-3. Files app
- 1. Right sidebar.
- 1. Panels. ExtensionPointId `app.files.sidebar`. Mounts extensions of type `sidebarPanel`. Used in any file(s) context (files app, file viewer apps, file editor apps).
- 2. File details table. ExtensionPointId `app.files.sidebar.file-details.table`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 3. Space details table. ExtensionPointId `app.files.sidebar.space-details.table`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 4. Shares panel people list top section. ExtensionPointId `app.files.sidebar.shares-panel.shared-with.top`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 5. Shares panel people list bottom section. ExtensionPointId `app.files.sidebar.shares-panel.shared-with.bottom`. Mounts extensions of type `customComponent`. Properties `space` and `resource` can be retrieved via injection context.
- 2. Folder views for regular folders. ExtensionPointId `app.files.folder-views.folder`. Mounts extensions of type `folderView`.
- 3. Folder views for the project spaces overview. ExtensionPointId `app.files.folder-views.project-spaces`. Mounts extensions of type `folderView`.
- 4. Folder views for the favorites page. ExtensionPointId `app.files.folder-views.favorites`. Mounts extensions of type `folderView`.
- 5. Right click context menu. ExtensionPointId `global.files.context-actions`. Mounts extensions of type `action`.
- 6. Batch actions in the app bar above file lists. ExtensionPointId `global.files.batch-actions`. Mounts extensions of type `action`.
- 7. Default actions (left click) on a file. ExtensionPointId `global.files.default-actions`. Mounts extensions of type `action`.
- 8. Upload menu. ExtensionPointId `app.files.upload-menu`. Mounts extensions of type `action`.
- 9. Quick actions. ExtensionPointId `app.files.quick-actions`. Mounts extensions of type `action`.
- 10. Quick actions for the trash overview. ExtensionPointId `app.files.trash-quick-actions`. Mounts extensions of type `action`.
-4. Global search providers. ExtensionPointId `app.search.providers`. Utilizes extensions of type `search` as search engines for the search input in the global top bar.
-5. User preference panels. ExtensionPointId `app.runtime.preferences.panels`. Mounts extensions of type `customComponent`.
-
-#### User Preferences for Extensions
-
-To allow users to configure extensions, extension points can define user preferences. User preferences are defined as an object on the extension point configuration.
-Whenever an extension point declares to accept user preferences, it will get listed with a dropdown on the Preferences page (reachable via top right user menu).
-The user can then select one out of all the extensions which have been registered for this extension point.
-
-#### Creating a new extension
-
-Please check out the [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) for a boilerplate application that also includes an extension. In addition to that, the [extension types docs](./extension-types/) provide instructions and examples on how to implement the different extension types.
-
-### Helpful packages
-
-We currently offer the following packages that can be integrated into your app, providing useful utilities and types.
-
-- `extension-sdk` - This package provides a default vite config that can be used when developing applications and extensions for the OpenCloud Web ecosystem. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/extension-sdk/README.md).
-- `web-client` - This package serves as an abstraction layer between the server APIs and an app or extension. It converts raw API data into objects with helpful types and utilities. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-client/README.md).
-- `web-pkg` - This package provides utilities, most importantly a variety of components and composables, that can be useful when developing apps and extensions. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-pkg/README.md).
-- `web-test-helpers` - This package provides utilities for writing unit tests. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-test-helpers/README.md).
+To learn how to integrate an app into OpenCloud Web, please refer to the
+[Web application admin docs](../../../admin/configuration/web-applications). To learn how to ship your app to users,
+please refer to the [build and publish docs](./build-and-publish).
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/packages.md b/versioned_docs/version-7.2/dev/web/extension-system/packages.md
new file mode 100644
index 000000000..7301d7ff7
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/packages.md
@@ -0,0 +1,21 @@
+---
+title: 'Helpful packages'
+sidebar_position: 6
+---
+
+We currently offer the following packages that can be integrated into your app, providing useful utilities and types.
+All of them are published on npm under the `@opencloud-eu` scope.
+
+## Runtime
+
+- `web-pkg` - This package provides utilities, most importantly a variety of components and composables, that can be useful when developing apps and extensions. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/web-pkg/README.md).
+- `web-client` - This package serves as an abstraction layer between the server APIs and an app or extension. It converts raw API data into objects with helpful types and utilities. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/web-client/README.md).
+- `design-system` - This package provides the components and utilities that OpenCloud Web is built with. Use it to make your app look and behave like the rest of the platform. For details, please refer to the [design system documentation](../design-system).
+
+## Tooling
+
+- `extension-sdk` - This package provides a default vite config that can be used when developing apps and extensions for the OpenCloud Web ecosystem. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/extension-sdk/README.md).
+- `tsconfig` - This package provides a reusable TypeScript config. Extend it in your `tsconfig.json`. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/tsconfig/README.md).
+- `eslint-config` - This package provides a reusable ESLint config. Extend it in your `eslint.config.js`. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/eslint-config/README.md).
+- `prettier-config` - This package provides a reusable Prettier config. Extend it in your `.prettierrc.json`. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/prettier-config/README.md).
+- `web-test-helpers` - This package provides utilities for writing unit tests. For details, please refer to the package's [README.md](https://github.com/opencloud-eu/web/blob/stable-7.1/packages/web-test-helpers/README.md).
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps/_category_.json b/versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps/_category_.json
new file mode 100644
index 000000000..30fd88bbc
--- /dev/null
+++ b/versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Viewer and editor apps",
+ "position": 5
+}
diff --git a/versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps.md b/versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps/index.md
similarity index 73%
rename from versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps.md
rename to versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps/index.md
index b70535907..69e16ca75 100644
--- a/versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps.md
+++ b/versioned_docs/version-7.2/dev/web/extension-system/viewer-editor-apps/index.md
@@ -1,28 +1,18 @@
---
title: 'Viewer and editor apps'
-sidebar_position: 1
---
-## Viewer and editor apps
-
OpenCloud Web allows developers to implement apps for viewing and editing specific file types. For instance, the built-in preview app serves as the default application for opening media files like images, videos, or audio.
This section will guide you through the process of implementing such an app within OpenCloud Web.
-### Basic app structure
-
-An app is essentially a distinct package that must be specified as an external application in the Web configuration.
-
-The structure of an app is quite simple and straightforward. Consider, for example, the [pdf-viewer app](https://github.com/opencloud-eu/web/tree/main/packages/web-app-pdf-viewer). It consists of a `package.json` file, a `src` directory containing all the source code, and a `l10n` directory for translations. Optionally, you may also include a `tests` directory if your application requires testing.
-
-To learn more about apps in general, please refer to the [Web app docs](../extension-system/).
+## App setup
-### App setup
+The `src/index.ts` file for a viewer or editor app may look like this:
-Inside the `src` folder you will need an `index.ts` file that sets up the app so it can be registered by the Web runtime. It follows the basic structure as described in [the apps section](../extension-system/), so it may look like this:
-
-```typescript
+```typescript title="src/index.ts"
import { AppWrapperRoute, defineWebApplication, AppMenuItemExtension } from '@opencloud-eu/web-pkg';
+import { urlJoin } from '@opencloud-eu/web-client';
import translations from '../l10n/translations.json';
import { useGettext } from 'vue3-gettext';
import { computed } from 'vue';
@@ -57,11 +47,13 @@ export default defineWebApplication({
// if you want your app to be present in the app menu on the top left.
const menuItems = computed(() => [
{
+ id: `app.${appId}.menuItem`,
label: () => $gettext('Advanced PDF Viewer'),
type: 'appMenuItem',
- handler: () => {
- // do stuff...
- }
+ color: '#ffffff',
+ icon: 'file-pdf',
+ priority: 30,
+ path: urlJoin(appId)
}
]);
@@ -100,17 +92,19 @@ Here is the interface defining the `extensions` property of the `appInfo` object
interface ApplicationFileExtension {
app?: string;
extension?: string;
+ type?: 'file' | 'folder';
createFileHandler?: (arg: {
fileName: string;
space: SpaceResource;
currentFolder: Resource;
}) => Promise;
hasPriority?: boolean;
- label?: string;
+ label?: string | (() => string);
name?: string;
icon?: string;
mimeType?: string;
newFileMenu?: { menuTitle: () => string };
routeName?: string;
+ secureView?: boolean;
}
```
diff --git a/versioned_docs/version-7.2/dev/web/getting-started.md b/versioned_docs/version-7.2/dev/web/getting-started.md
index 2524a93b5..9cd25082c 100644
--- a/versioned_docs/version-7.2/dev/web/getting-started.md
+++ b/versioned_docs/version-7.2/dev/web/getting-started.md
@@ -13,7 +13,9 @@ To install and setup the Web client on your local machine, please refer to the [
## Configuration
-Web can be configured using a configuration file in `json` format. Sample configuration files are available in the [config folder](https://github.com/opencloud-eu/web/tree/main/config) of the OpenCloud Web Git repository. Below is a detailed overview of all available configuration options.
+Web can be configured using a configuration file in `json` format. This is completely optional, as default settings are used when no configuration file is provided. You need to tell the server where to find a custom configuration file via the `WEB_UI_CONFIG_FILE` environment variable.
+
+Below is a detailed overview of all available configuration options.
### `server`
@@ -23,30 +25,6 @@ Specifies the server URL, e.g. `https://host.docker.internal:9200`.
Specifies the URL for the theme to be loaded, e.g. `https://host.docker.internal:9200/themes/opencloud/theme.json`.
-### `options`
-
-General options that control the behavior of the Web client. Expects an object with the following possible options:
-
-- `options.accountEditLink` This accepts an object with the following optional fields to have a link on the account page:
- - `options.accountEditLink.href` Set a different target URL for the edit link. Make sure to prepend it with `http(s)://`.
-- `options.sharingRecipientsPerPage` Sets the amount of users shown as recipients in the dropdown when sharing resources. Default amount is 200.
-- `options.runningOnEos` Set this option to `true` if running on an [EOS storage backend](https://eos-web.web.cern.ch/eos-web/) to enable its specific features. Defaults to `false`.
-- `options.cernFeatures` Enabling this will activate CERN-specific features. Defaults to `false`.
-- `options.editor.autosaveEnabled` Specifies if the autosave for the editor apps is enabled.
-- `options.editor.autosaveInterval` Specifies the time interval for the autosave of editor apps in seconds.
-- `options.editor.openAsPreview` Specifies if non-personal files i.e. files in shares, spaces or public links are being opened in read only mode so the user needs to manually switch to edit mode. Can be set to `true`, `false` or an array of web app/editor names.
-- `options.contextHelpersReadMore` Specifies whether the "Read more" link should be displayed or not.
-- `options.tokenStorageLocal` Specifies whether the access token will be stored in the local storage when set to `true` or in the session storage when set to `false`. If stored in the local storage, login state will be persisted across multiple browser tabs, means no additional logins are required. Defaults to `true`.
-- `options.loginUrl` Specifies the target URL to the login page. This is helpful when an external IdP is used. This option is disabled by default. Example URL like: '[https://www.myidp.com/login](https://www.myidp.com/login)'.
-- `options.logoutUrl` Adds a link to the user's profile page to point him to an external page, where he can manage his session and devices. This is helpful when an external IdP is used. This option is disabled by default.
-- `options.userListRequiresFilter` Defines whether one or more filters must be set in order to list users in the Web admin settings. Set this option to 'true' if running in an environment with a lot of users and listing all users could slow down performance. Defaults to `false`.
-- `options.concurrentRequests` This accepts an object with the following optional fields to customize the maximum number of concurrent requests in code paths where we limit concurrent requests
- - `resourceBatchActions` Concurrent number of file/folder/space batch actions like e.g. accepting shares. Defaults to 4.
- - `sse` Concurrent number of SSE event handlers. Defaults to 4.
- - `shares` Accepts an object regarding the following sharing related options:
- - `create` Concurrent number of share invites. Defaults to 4.
- - `list` Concurrent number of individually loaded shares. Defaults to 2.
-
### `apps`
Controls the Web apps to be loaded. This is not for adding external apps, but for specifying which of the internal apps that are shipped with Web should be loaded. Expects a list of strings, e.g.: