# File Manager Collabora / WOPI Flow Dokumen ini menjelaskan alur frontend untuk membuka editor Collabora lewat backend WOPI. Prinsip utama: 1. Frontend hanya memanggil App API. 2. Frontend tidak pernah memanggil endpoint `/wopi/...` secara langsung. 3. Backend yang mengurus token, proof, S3, lock, autosave, dan finalisasi. Tambahan penting untuk flow helicopter template: 1. hasil `create-from-template` akan dibuat sebagai file manager file biasa, bukan template 2. backend juga akan memastikan file itu masuk ke `helicopter_files` section `fpwb` 3. kalau frontend membuka template yang sama untuk `takeover_id` yang sama, backend akan reuse file existing bila sudah ada 4. saat child `fpwb` baru berhasil dibuat, backend akan kirim SSE agar frontend bisa langsung append item baru ke list 5. kalau file hasil template punya `takeover_id`, endpoint `GET /api/v1/helicopter-files/get/{helicopter_id}` dan `GET /api/v1/helicopter-files/list` akan menyembunyikan item itu dari user yang tidak punya akses takeover tersebut ## Endpoint Yang Dipakai Frontend ### 1) Buat file baru dari template `POST /api/v1/file-manager/files/create-from-template` Request: ```json { "data": { "type": "file_manager_file_create_from_template", "attributes": { "template_uuid": "019f0386-0a00-7a4a-a2d8-30afc0e0c3ad", "helicopter_id": "019e8713-aa19-7fb7-a510-a0332dc69697", "takeover_id": "019f0386-0a00-7a4a-a2d8-30afc0e0c3af", "name": "collabora-draft.docx" } } } ``` Response: ```json { "data": { "file_id": "019f0a12-1111-7abc-8def-222222222222", "collabora_action": "https://collabora.example.com/.../WOPISrc=...", "access_token": "eyJhbGciOiJIUzI1NiIs...", "access_token_ttl": 1800000 } } ``` ### 2) Buka editor untuk file existing `GET /api/v1/file-manager/files/{fileId}/edit-session` Response shape sama: ```json { "data": { "file_id": "019f0a12-1111-7abc-8def-222222222222", "collabora_action": "https://collabora.example.com/.../WOPISrc=...", "access_token": "eyJhbGciOiJIUzI1NiIs...", "access_token_ttl": 1800000 } } ``` ### 3) Ambil daftar helicopter files `GET /api/v1/helicopter-files/get/{helicopter_id}` Endpoint ini yang dipakai frontend untuk melihat child file yang muncul di helicopter file collection. Untuk hasil `create-from-template`, item baru akan muncul di: 1. `data.attributes.files.prepare` 2. section backend = `fpwb` 3. secara UI biasanya ditampilkan sebagai area Flight Preparation and WB ### 4) Dengarkan event child baru `GET /api/v1/helicopter-files/events?helicopter_id={helicopter_id}` Stream ini dipakai supaya frontend bisa langsung append child baru tanpa reload penuh. Event name: `helicopter.file.created` ### SSE dapet dari mana? SSE ini bukan datang dari Collabora, bukan dari browser local state, dan bukan dari endpoint `/wopi/...`. Sumbernya adalah backend App API: 1. frontend subscribe ke `GET /api/v1/helicopter-files/events?helicopter_id={helicopter_id}` 2. route ini masuk ke handler SSE backend 3. backend hanya akan mengirim event ke user yang sedang subscribe 4. event difilter lagi dengan `helicopter_id` query param Kapan event dipublish: 1. frontend memanggil `POST /api/v1/file-manager/files/create-from-template` 2. backend membuat atau reuse file manager file 3. backend memastikan ada row `helicopter_files` di section `fpwb` 4. kalau row `helicopter_files` itu baru dibuat saat request ini, backend publish event `helicopter.file.created` 5. stream `/api/v1/helicopter-files/events` mengirim event itu ke frontend yang subscribe Artinya: 1. kalau child `fpwb` sudah ada dari sebelumnya, backend tidak akan publish event create lagi 2. kalau frontend telat subscribe, fallback paling aman tetap re-fetch `GET /api/v1/helicopter-files/get/{helicopter_id}` 3. SSE ini hanya untuk sinkronisasi UI realtime, bukan source of truth utama ## Flow Create From Template ### Step 1 - Frontend minta editor session Frontend mengirim `template_uuid`, `helicopter_id`, `name`, dan boleh menambahkan `takeover_id`. `template_uuid` harus mengarah ke `helicopter_file.helicopter_file_id` dari helicopter yang sama dengan `helicopter_id`, bukan ke file takeover biasa. Backend akan menjalankan alur seperti ini: ```text create-from-template -> cek user session -> cek template memang milik helicopter itu -> jika ada takeover_id: -> cek apakah kombinasi template + takeover sudah punya file existing -> jika sudah ada: -> reuse file existing -> pastikan metadata template + takeover tersimpan -> pastikan ada child helicopter_files di section fpwb -> jika child baru dibuat, publish SSE helicopter.file.created -> mint WOPI token -> balikin Collabora action URL -> jika belum ada: -> buat draft file baru dari template -> copy object template ke key final file baru -> simpan metadata template + takeover -> pastikan ada child helicopter_files di section fpwb -> jika child baru dibuat, publish SSE helicopter.file.created -> mint WOPI token -> balikin Collabora action URL -> jika takeover_id tidak ada: -> buat draft file baru dari template -> copy object template ke key final file baru -> simpan metadata template -> pastikan ada child helicopter_files di section fpwb -> jika child baru dibuat, publish SSE helicopter.file.created -> mint WOPI token -> balikin Collabora action URL ``` Catatan untuk step 9: 1. SSE dipublish oleh backend App API pada request `create-from-template` 2. jadi FE yang sedang membuka halaman helicopter files bisa langsung menerima event tanpa menunggu save dari Collabora 3. source of truth event tetap berasal dari proses create child row di backend ### Step 2 - Frontend simpan response Frontend wajib menyimpan: 1. `file_id` 2. `collabora_action` 3. `access_token` 4. `access_token_ttl` dalam milidetik `file_id` dipakai sebagai join key untuk session berikutnya. `file_id` ini juga bisa dikirim lagi saat frontend create takeover lewat field `edited_template_file_ids`. Kalau tujuan frontend adalah menampilkan child di helicopter files: 1. `file_id` adalah ID file manager file 2. `helicopter_file_id` tidak ada di response create-from-template 3. `helicopter_file_id` akan muncul lewat: `GET /api/v1/helicopter-files/get/{helicopter_id}` atau SSE `helicopter.file.created` ### Step 3 - Frontend buka Collabora Frontend tidak boleh hanya redirect atau `window.open(collabora_action)` begitu saja. `collabora_action` harus dibuka dengan `POST` form yang ikut mengirim: 1. `access_token` 2. `access_token_ttl` dalam milidetik Ini penting karena backend WOPI membaca token dari query `access_token` atau header bearer pada request WOPI yang datang dari Collabora. Kalau frontend hanya membuka `collabora_action` tanpa token bootstrap, backend akan mengembalikan error seperti: 1. `401 FILE_MANAGER_WOPI_INVALID_TOKEN` 2. `token is malformed: token contains an invalid number of segments` Frontend membuka editor memakai: 1. `collabora_action` sebagai `form action` 2. `access_token` sebagai hidden input 3. `access_token_ttl` sebagai hidden input Rule praktis: 1. jangan pernah memanggil `/wopi/...` dari browser 2. jangan pernah expose S3 URL atau credential ke frontend 3. gunakan `collabora_action` sebagai base URL editor 4. jangan buka `collabora_action` tanpa submit `access_token` ## File Akan Muncul Di Mana Setelah `create-from-template` sukses, hasil akhirnya ada di dua tempat berbeda yang perlu dibedakan: ### 1) File manager file Ini adalah object file utama yang dibuka oleh Collabora. Identifier utamanya: 1. `file_id` Endpoint yang relevan: 1. `GET /api/v1/file-manager/files/{fileId}/edit-session` 2. WOPI internal backend `/wopi/files/{fileId}` dan `/wopi/files/{fileId}/contents` ### 2) Helicopter file child Ini adalah row bisnis yang membuat file tadi muncul di halaman helicopter files. Lokasinya: 1. `GET /api/v1/helicopter-files/get/{helicopter_id}` 2. response path `data.attributes.files.prepare` 3. section row = `fpwb` Field penting item collection: 1. `helicopter_file_id` 2. `attachment.id` 3. `attachment.file_name` Catatan: 1. child ini bukan parent folder file manager 2. child ini adalah row di tabel `helicopter_files` 3. relasinya menunjuk ke file hasil template lewat `source_file_id` ## Contoh Integrasi Next.js Bagian ini menunjukkan contoh yang lengkap untuk Next.js App Router. Asumsi: 1. user sudah login ke App API 2. frontend memanggil backend App API, bukan endpoint `/wopi/...` 3. backend mengembalikan `file_id`, `collabora_action`, `access_token`, dan `access_token_ttl` ### Tipe response ```ts export type CollaboraSessionResponse = { data: { file_id: string collabora_action: string access_token: string access_token_ttl: number } } ``` ### Server action atau API helper Contoh helper untuk memanggil backend `create-from-template`. ```ts // app/lib/file-manager.ts export type CreateFromTemplateInput = { templateUuid: string helicopterId: string takeoverId?: string name: string } export type CollaboraSessionResponse = { data: { file_id: string collabora_action: string access_token: string access_token_ttl: number } } export async function createFromTemplate( input: CreateFromTemplateInput, accessToken: string, ): Promise { const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/file-manager/files/create-from-template`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ data: { type: "file_manager_file_create_from_template", attributes: { template_uuid: input.templateUuid, helicopter_id: input.helicopterId, takeover_id: input.takeoverId, name: input.name, }, }, }), cache: "no-store", }, ) if (!response.ok) { throw new Error(`Create from template failed with status ${response.status}`) } return response.json() } ``` Contoh helper untuk membuka file existing: ```ts // app/lib/file-manager.ts export async function getEditSession( fileId: string, accessToken: string, ): Promise { const response = await fetch( `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/file-manager/files/${fileId}/edit-session`, { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, }, cache: "no-store", }, ) if (!response.ok) { throw new Error(`Edit session failed with status ${response.status}`) } return response.json() } ``` ### Komponen client untuk bootstrap Collabora Komponen ini membuat form hidden dan auto-submit ke `collabora_action`. ```tsx // app/components/collabora-launcher.tsx "use client" import { useEffect, useRef } from "react" type Props = { collaboraAction: string accessToken: string accessTokenTTL: number // milliseconds target?: string } export function CollaboraLauncher({ collaboraAction, accessToken, accessTokenTTL, target = "collabora-editor", }: Props) { const formRef = useRef(null) useEffect(() => { formRef.current?.submit() }, []) return ( <>