# Colaboración

- Human documentation: [https://docs.univer.ai/es-ES/guides/sheets/features/collaboration](https://docs.univer.ai/es-ES/guides/sheets/features/collaboration)

- Agent Markdown: [https://docs.univer.ai/es-ES/guides/sheets/features/collaboration.md](https://docs.univer.ai/es-ES/guides/sheets/features/collaboration.md)

- Requested language: `es-ES`

- Content language: `es-ES`

- Documentation version: `1.2.0`

- Source: [sheets/features/collaboration.es-ES.mdx](https://github.com/dream-num/documentation/blob/dev/content/guides/sheets/features/collaboration.es-ES.mdx)

---

#### Package metadata

```json
{
  "preset": [
    {
      "client": "@univerjs/preset-sheets-collaboration",
      "locale": "@univerjs/preset-sheets-collaboration/locales/es-ES",
      "style": "@univerjs/preset-sheets-collaboration/lib/index.css"
    }
  ],
  "plugins": [
    {
      "client": "@univerjs-pro/collaboration"
    },
    {
      "client": "@univerjs-pro/collaboration-client",
      "facade": "@univerjs-pro/collaboration-client/facade",
      "locale": "@univerjs-pro/collaboration-client/locale/es-ES"
    },
    {
      "client": "@univerjs-pro/collaboration-client-ui",
      "facade": "@univerjs-pro/collaboration-client-ui/facade",
      "locale": "@univerjs-pro/collaboration-client-ui/locale/es-ES",
      "style": "@univerjs-pro/collaboration-client-ui/lib/index.css"
    }
  ],
  "license": true,
  "server": true
}
```

Sigue la [Integración colaborativa](https://docs.univer.ai/es-ES/server/collaboration/browser-integration.md) para ejecutar el ejemplo, conectar tu aplicación y verificar persistencia y permisos. Los detalles y ejemplos Web/Server están en [Inicio rápido de colaboración](https://docs.univer.ai/es-ES/server/collaboration/quick-start.md).

Crea el Unit mediante una API de negocio autenticada que invoque Collaboration Service y carga el ID devuelto en el navegador. Configura HTTP, Session Ticket y WebSocket según [Inicio rápido de colaboración](https://docs.univer.ai/es-ES/server/collaboration/quick-start.md), persiste con un Database Adapter y protege lectura, JOIN y edición mediante [permisos del servidor](https://docs.univer.ai/es-ES/server/collaboration/identity-and-authorization.md).

## Cargar el documento

Establece el usuario actual antes de cargar un documento. Identifica al colaborador y es propietario de la caché local sin conexión. Una vez establecido, carga el documento explícitamente con la Facade:

```ts
import { UserManagerService } from '@univerjs/core'
import '@univerjs-pro/collaboration-client/facade'

univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })

const collaboration = univerAPI.getCollaboration()
const unit = await collaboration.loadSheetAsync('your-unit-id')
```

Usa [estado, reenvíos y recuperación de conflictos](https://docs.univer.ai/es-ES/server/collaboration/browser-integration.md#sync-status) para indicar el guardado y proteger cambios sin confirmar antes de recargar.

## Eliminación de la carga integrada por URL de documentos colaborativos

> [!WARNING: Nota]
> A partir de la versión **1.2.0**, Univer ha eliminado la carga integrada por URL de documentos colaborativos. Ya no lee automáticamente los parámetros `unit`, `type` y `subunit` de la URL para cargar un documento colaborativo y activar una hoja.

Si tu aplicación depende de este comportamiento, puedes recuperarlo mediante un plugin personalizado.

### Integrar el plugin personalizado

Copia los cuatro archivos siguientes en tu proyecto con esta estructura de directorios:

```text
custom-url-load/
├── plugin.ts
├── controllers/
│   └── data-loader.controller.ts
└── services/
    ├── url.service.ts
    └── web-url.service.ts
```

Después de inicializar el editor y los plugins de colaboración, establece el usuario actual antes de registrar `CustomUrlLoadPlugin`. Esta integración funciona tanto en modo plugin como en modo preset:

```ts
import { UserManagerService } from '@univerjs/core'
import { CustomUrlLoadPlugin } from './custom-url-load/plugin'

univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })

univer.registerPlugin(CustomUrlLoadPlugin)
```

Una vez registrado, el plugin carga el documento, por lo que no necesitas llamar a `loadSheetAsync()` para el mismo documento. Si tu aplicación ya carga documentos mediante la Facade, puedes mantener el enfoque de la sección anterior y gestionar las URL según las necesidades de tu aplicación.

### Código completo del plugin

#### plugin.ts

```ts
import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'
import { DependentOn, Inject, Injector, Plugin } from '@univerjs/core'
import { CollaborationDataLoaderController } from './controllers/data-loader.controller'
import { IURLService } from './services/url.service'
import { WebURLService } from './services/web-url.service'

@DependentOn(UniverCollaborationClientPlugin)
export class CustomUrlLoadPlugin extends Plugin {
  static override pluginName = 'CUSTOM_URL_LOAD_PLUGIN'

  constructor(
    _config: undefined,
    @Inject(Injector) protected override readonly _injector: Injector
  ) {
    super()
  }

  override onStarting(): void {
    this._injector.add([IURLService, { useClass: WebURLService }])
    this._injector.add([CollaborationDataLoaderController])

    this.disposeWithMe(this._injector.get(IURLService))
    this.disposeWithMe(this._injector.get(CollaborationDataLoaderController))
  }
}
```

#### controllers/data-loader.controller.ts

```ts
import type { ISetWorksheetActivateCommandParams } from '@univerjs/sheets'
import type { Subscription } from 'rxjs'
import { DataLoaderService } from '@univerjs-pro/collaboration-client'
import {
  ICommandService,
  ILogService,
  Inject,
  IUniverInstanceService,
  RxDisposable,
  toDisposable,
  UniverInstanceType,
  Workbook,
} from '@univerjs/core'
import { SetWorksheetActivateCommand } from '@univerjs/sheets'
import { takeUntil } from 'rxjs'
import { IURLService } from '../services/url.service'

/**
 * Carga un documento colaborativo desde los parámetros de la URL y sincroniza la navegación entre sus hojas.
 */
export class CollaborationDataLoaderController extends RxDisposable {
  private _unitId: string | undefined
  private _unitType = UniverInstanceType.UNIVER_SHEET
  private _workbook: Workbook | null = null
  private _activeSheetSubscription: Subscription | null = null

  constructor(
    @IURLService private readonly _urlService: IURLService,
    @ILogService private readonly _logService: ILogService,
    @ICommandService private readonly _commandService: ICommandService,
    @Inject(DataLoaderService) private readonly _dataLoaderService: DataLoaderService,
    @IUniverInstanceService private readonly _instanceService: IUniverInstanceService
  ) {
    super()
    this._init()
  }

  private _init(): void {
    this._unitId = this._urlService.getParam('unit')
    const type = this._urlService.getParam('type')
    if (!this._unitId || !type) {
      return
    }

    this._unitType = Number(type)
    this.disposeWithMe(toDisposable(() => this._unbindWorkbook()))

    // Restaurar una versión puede sustituir el Workbook conservando el ID del documento, por lo que hay que enlazarlo de nuevo.
    this._dataLoaderService.unitLoaded$.pipe(takeUntil(this.dispose$)).subscribe((unit) => {
      if (unit instanceof Workbook && unit.getUnitId() === this._unitId) {
        this._bindWorkbook(unit).catch((error: unknown) => this._reportError(error))
      }
    })
    this._instanceService.unitDisposed$.pipe(takeUntil(this.dispose$)).subscribe((unit) => {
      if (unit === this._workbook) {
        this._unbindWorkbook()
      }
    })
    this._urlService.urlChange$.pipe(takeUntil(this.dispose$)).subscribe(() => {
      if (this._workbook) {
        this._activateWorksheetFromUrl(this._workbook).catch((error: unknown) => this._reportError(error))
      }
    })

    this._dataLoaderService.loadUnit(this._unitId, this._unitType, this._urlService.getParam('subunit'))
      .catch((error: unknown) => this._reportError(error))
  }

  private async _bindWorkbook(workbook: Workbook): Promise<void> {
    this._unbindWorkbook()
    this._workbook = workbook
    // La reproducción de la instantánea actualiza el modelo directamente; activa la hoja una vez para notificar al renderizador.
    await this._activateWorksheetFromUrl(workbook, true)
    if (this._workbook !== workbook || this._disposed) {
      return
    }

    this._activeSheetSubscription = workbook.activeSheet$.subscribe((worksheet) => {
      if (worksheet) {
        this._updateUrl(worksheet.getSheetId())
      }
    })
  }

  private _unbindWorkbook(): void {
    this._activeSheetSubscription?.unsubscribe()
    this._activeSheetSubscription = null
    this._workbook = null
  }

  private _matchesDocument(): boolean {
    return this._urlService.getParam('unit') === this._unitId
      && Number(this._urlService.getParam('type')) === this._unitType
  }

  private async _activateWorksheetFromUrl(workbook: Workbook, forceActivate = false): Promise<void> {
    if (this._disposed || this._workbook !== workbook || !this._matchesDocument()) {
      return
    }

    const requestedSheetId = this._urlService.getParam('subunit')
    const worksheet = (requestedSheetId ? workbook.getSheetBySheetId(requestedSheetId) : null)
      ?? workbook.getSheetBySheetId(workbook.getUnhiddenWorksheets()[0])
    if (!worksheet) {
      return
    }

    const subUnitId = worksheet.getSheetId()
    if (subUnitId !== requestedSheetId) {
      this._updateUrl(subUnitId, true)
    }
    if (!forceActivate && workbook.getActiveSheet()?.getSheetId() === subUnitId) {
      return
    }

    await this._commandService.executeCommand<ISetWorksheetActivateCommandParams>(SetWorksheetActivateCommand.id, {
      unitId: workbook.getUnitId(),
      subUnitId,
    })
  }

  private _updateUrl(subUnitId: string, replace = false): void {
    if (this._matchesDocument() && this._urlService.getParam('subunit') !== subUnitId) {
      this._urlService.setParam('subunit', subUnitId, replace)
    }
  }

  private _reportError(error: unknown): void {
    this._logService.error('[CollaborationDataLoaderController]', error)
  }
}
```

#### services/url.service.ts

```ts
import type { IDisposable } from '@univerjs/core'
import type { Observable } from 'rxjs'
import { createIdentifier } from '@univerjs/core'

export interface IURLService extends IDisposable {
  getParam: (key: string) => string | undefined
  setParam: (key: string, value: string, useReplace?: boolean) => void
  removeParam: (key: string, useReplace?: boolean) => void
  readonly urlChange$: Observable<void>
}

export const IURLService = createIdentifier<IURLService>('demo.custom-url-load.url-service')
```

#### services/web-url.service.ts

```ts
import type { IURLService } from './url.service'
import { RxDisposable } from '@univerjs/core'
import { fromEvent, map, shareReplay, takeUntil } from 'rxjs'

export class WebURLService extends RxDisposable implements IURLService {
  readonly urlChange$ = fromEvent(window, 'popstate').pipe(
    map(() => undefined),
    takeUntil(this.dispose$),
    shareReplay({ bufferSize: 1, refCount: true })
  )

  getParam(key: string): string | undefined {
    return new URL(window.location.href).searchParams.get(key) ?? undefined
  }

  setParam(key: string, value: string, useReplace = false): void {
    const url = new URL(window.location.href)
    url.searchParams.set(key, value)
    this._updateUrl(url, useReplace)
  }

  removeParam(key: string, useReplace = false): void {
    const url = new URL(window.location.href)
    url.searchParams.delete(key)
    this._updateUrl(url, useReplace)
  }

  private _updateUrl(url: URL, useReplace: boolean): void {
    if (useReplace) {
      window.history.replaceState(window.history.state, '', url.toString())
    } else {
      window.history.pushState(window.history.state, '', url.toString())
    }
  }
}
```

[Node.js / AI SDK](https://docs.univer.ai/es-ES/ai/content-operations.md) · [OT](/blog/ot)
