830 lines
23 KiB
Markdown
830 lines
23 KiB
Markdown
# 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<CollaboraSessionResponse> {
|
|
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<CollaboraSessionResponse> {
|
|
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<HTMLFormElement | null>(null)
|
|
|
|
useEffect(() => {
|
|
formRef.current?.submit()
|
|
}, [])
|
|
|
|
return (
|
|
<>
|
|
<form ref={formRef} action={collaboraAction} method="post" target={target}>
|
|
<input type="hidden" name="access_token" value={accessToken} />
|
|
<input type="hidden" name="access_token_ttl" value={String(accessTokenTTL)} />
|
|
</form>
|
|
|
|
<iframe
|
|
name={target}
|
|
title="Collabora Editor"
|
|
className="h-[calc(100vh-120px)] w-full border-0"
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Halaman Next.js untuk create from template
|
|
|
|
Contoh berikut memakai App Router dan menampilkan editor setelah session berhasil dibuat.
|
|
|
|
```tsx
|
|
// app/file-manager/templates/[templateId]/open/page.tsx
|
|
import { CollaboraLauncher } from "@/app/components/collabora-launcher"
|
|
import { createFromTemplate } from "@/app/lib/file-manager"
|
|
import { cookies } from "next/headers"
|
|
|
|
type PageProps = {
|
|
params: Promise<{
|
|
templateId: string
|
|
}>
|
|
searchParams: Promise<{
|
|
helicopterId?: string
|
|
name?: string
|
|
}>
|
|
}
|
|
|
|
export default async function OpenTemplatePage({ params, searchParams }: PageProps) {
|
|
const { templateId } = await params
|
|
const { helicopterId, name } = await searchParams
|
|
|
|
if (!helicopterId || !name) {
|
|
throw new Error("helicopterId and name are required")
|
|
}
|
|
|
|
const cookieStore = await cookies()
|
|
const accessToken = cookieStore.get("access_token")?.value
|
|
if (!accessToken) {
|
|
throw new Error("Missing app access token")
|
|
}
|
|
|
|
const session = await createFromTemplate(
|
|
{
|
|
templateUuid: templateId,
|
|
helicopterId,
|
|
name,
|
|
},
|
|
accessToken,
|
|
)
|
|
|
|
return (
|
|
<CollaboraLauncher
|
|
collaboraAction={session.data.collabora_action}
|
|
accessToken={session.data.access_token}
|
|
accessTokenTTL={session.data.access_token_ttl}
|
|
/>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Halaman Next.js untuk file existing
|
|
|
|
```tsx
|
|
// app/file-manager/files/[fileId]/edit/page.tsx
|
|
import { CollaboraLauncher } from "@/app/components/collabora-launcher"
|
|
import { getEditSession } from "@/app/lib/file-manager"
|
|
import { cookies } from "next/headers"
|
|
|
|
type PageProps = {
|
|
params: Promise<{
|
|
fileId: string
|
|
}>
|
|
}
|
|
|
|
export default async function EditFilePage({ params }: PageProps) {
|
|
const { fileId } = await params
|
|
|
|
const cookieStore = await cookies()
|
|
const accessToken = cookieStore.get("access_token")?.value
|
|
if (!accessToken) {
|
|
throw new Error("Missing app access token")
|
|
}
|
|
|
|
const session = await getEditSession(fileId, accessToken)
|
|
|
|
return (
|
|
<CollaboraLauncher
|
|
collaboraAction={session.data.collabora_action}
|
|
accessToken={session.data.access_token}
|
|
accessTokenTTL={session.data.access_token_ttl}
|
|
/>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Kenapa tidak cukup pakai `window.open(collabora_action)`?
|
|
|
|
Karena response backend memisahkan:
|
|
|
|
1. `collabora_action`
|
|
2. `access_token`
|
|
3. `access_token_ttl`
|
|
|
|
Sedangkan backend WOPI membutuhkan token valid saat Collabora mulai memanggil:
|
|
|
|
1. `GET /wopi/files/{fileId}`
|
|
2. `GET /wopi/files/{fileId}/contents`
|
|
|
|
Kalau frontend hanya membuka URL editor tanpa mengirim hidden form fields di atas, Collabora akan tetap mencoba request WOPI, tetapi backend akan membaca token kosong atau malformed.
|
|
|
|
### Contoh yang salah
|
|
|
|
```tsx
|
|
window.open(session.data.collabora_action, "_blank")
|
|
```
|
|
|
|
atau:
|
|
|
|
```tsx
|
|
<iframe src={session.data.collabora_action} />
|
|
```
|
|
|
|
Kedua contoh di atas tidak mengirim `access_token` bootstrap.
|
|
|
|
### Contoh yang benar
|
|
|
|
```tsx
|
|
<form action={session.data.collabora_action} method="post" target="collabora-editor">
|
|
<input type="hidden" name="access_token" value={session.data.access_token} />
|
|
<input
|
|
type="hidden"
|
|
name="access_token_ttl"
|
|
value={String(session.data.access_token_ttl)}
|
|
/>
|
|
</form>
|
|
```
|
|
|
|
### Step 4 - Autosave berjalan di backend
|
|
|
|
Setelah editor terbuka:
|
|
|
|
1. Collabora memanggil `GET /wopi/files/{fileId}`
|
|
2. Collabora memanggil `GET /wopi/files/{fileId}/contents`
|
|
3. saat autosave, Collabora memanggil `POST /wopi/files/{fileId}/contents`
|
|
4. saat close / exit save, Collabora mengirim lifecycle headers
|
|
|
|
Frontend tidak perlu mengurus autosave secara manual.
|
|
|
|
## Flow Tampilan Child Di Helicopter Files
|
|
|
|
Kalau FE punya halaman detail helicopter files yang memanggil:
|
|
|
|
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
|
|
|
maka flow lengkapnya begini:
|
|
|
|
### Step 1 - Load data awal
|
|
|
|
Frontend fetch:
|
|
|
|
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
|
|
|
Lalu render:
|
|
|
|
1. `data.attributes.files.before`
|
|
2. `data.attributes.files.prepare`
|
|
3. `data.attributes.files.after`
|
|
|
|
Hasil `create-from-template` akan masuk ke array:
|
|
|
|
`data.attributes.files.prepare`
|
|
|
|
### Step 2 - Subscribe SSE
|
|
|
|
Saat halaman helicopter files terbuka, frontend subscribe:
|
|
|
|
`GET /api/v1/helicopter-files/events?helicopter_id={helicopter_id}`
|
|
|
|
Event yang perlu didengar:
|
|
|
|
`helicopter.file.created`
|
|
|
|
Penjelasan singkat:
|
|
|
|
1. endpoint ini adalah stream SSE dari backend App API
|
|
2. backend akan keep connection tetap hidup dan mengirim heartbeat
|
|
3. event akan dikirim ketika backend membuat child `helicopter_file` baru untuk helicopter itu
|
|
4. event ini tidak tergantung pada autosave WOPI
|
|
|
|
Contoh payload:
|
|
|
|
```json
|
|
{
|
|
"helicopter_id": "019dfb9e-db0c-7eaf-b701-8b4928e62d24",
|
|
"helicopter_file_id": "019f2000-aaaa-7bbb-8ccc-111111111111",
|
|
"file_id": "019f2000-dddd-7eee-8fff-222222222222",
|
|
"section": "fpwb",
|
|
"position": 3,
|
|
"is_mandatory": false,
|
|
"attachment_pending": true,
|
|
"name": "collabora-draft.docx",
|
|
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
"updated_at": "2026-07-01T08:30:00Z"
|
|
}
|
|
```
|
|
|
|
### Step 3 - Append child baru ke state
|
|
|
|
Kalau frontend menerima event:
|
|
|
|
1. cocokkan `helicopter_id`
|
|
2. cek `section === "fpwb"`
|
|
3. append ke collection `prepare`
|
|
4. hindari duplikasi dengan key `helicopter_file_id`
|
|
|
|
Contoh Next.js client hook:
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import { useEffect } from "react"
|
|
|
|
type HelicopterFileCreatedEvent = {
|
|
helicopter_id: string
|
|
helicopter_file_id: string
|
|
file_id: string
|
|
section: string
|
|
position: number
|
|
is_mandatory: boolean
|
|
attachment_pending: boolean
|
|
name: string
|
|
mime_type: string
|
|
updated_at: string
|
|
}
|
|
|
|
type PrepareItem = {
|
|
helicopter_file_id: string
|
|
section: string
|
|
position: number
|
|
is_mandatory: boolean
|
|
attachment_pending: boolean
|
|
attachment: {
|
|
id: string
|
|
file_name: string
|
|
download_url: string
|
|
}
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export function useHelicopterFileCreatedSSE(
|
|
helicopterId: string,
|
|
onCreated: (event: HelicopterFileCreatedEvent) => void,
|
|
) {
|
|
useEffect(() => {
|
|
const url = new URL(
|
|
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/helicopter-files/events`,
|
|
)
|
|
url.searchParams.set("helicopter_id", helicopterId)
|
|
|
|
const source = new EventSource(url.toString(), { withCredentials: true })
|
|
|
|
const handler = (raw: MessageEvent<string>) => {
|
|
const payload = JSON.parse(raw.data) as HelicopterFileCreatedEvent
|
|
onCreated(payload)
|
|
}
|
|
|
|
source.addEventListener("helicopter.file.created", handler as EventListener)
|
|
|
|
return () => {
|
|
source.removeEventListener(
|
|
"helicopter.file.created",
|
|
handler as EventListener,
|
|
)
|
|
source.close()
|
|
}
|
|
}, [helicopterId, onCreated])
|
|
}
|
|
```
|
|
|
|
Contoh append ke state:
|
|
|
|
```tsx
|
|
setPrepareFiles((current) => {
|
|
if (current.some((item) => item.helicopter_file_id === event.helicopter_file_id)) {
|
|
return current
|
|
}
|
|
|
|
const nextItem = {
|
|
helicopter_file_id: event.helicopter_file_id,
|
|
section: event.section,
|
|
position: event.position,
|
|
is_mandatory: event.is_mandatory,
|
|
attachment_pending: event.attachment_pending,
|
|
attachment: {
|
|
id: event.file_id,
|
|
file_name: event.name,
|
|
download_url: "",
|
|
},
|
|
created_at: event.updated_at,
|
|
updated_at: event.updated_at,
|
|
}
|
|
|
|
return [...current, nextItem].sort((a, b) => a.position - b.position)
|
|
})
|
|
```
|
|
|
|
Catatan:
|
|
|
|
1. saat event baru datang, `attachment_pending` bisa masih `true`
|
|
2. artinya child row sudah ada, tetapi attachment final mungkin belum ter-resolve penuh
|
|
3. kalau FE butuh shape yang benar-benar final, aman juga untuk re-fetch sekali ke:
|
|
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
|
|
|
### Step 5 - Refresh token saat TTL hampir habis
|
|
|
|
Token WOPI bersifat short-lived.
|
|
|
|
Saat token mendekati habis, frontend harus:
|
|
|
|
1. memanggil ulang `GET /api/v1/file-manager/files/{fileId}/edit-session` untuk file yang sudah ada
|
|
|
|
Lalu replace token yang dipakai untuk membuka editor.
|
|
|
|
Catatan:
|
|
|
|
1. setelah file pertama sukses dibuat, jangan panggil `create-from-template` lagi hanya untuk refresh token
|
|
2. untuk reopen atau refresh editor, gunakan `edit-session`
|
|
3. `create-from-template` dipakai untuk bootstrap file baru atau reuse by `template + takeover`
|
|
|
|
## Flow Edit File Existing
|
|
|
|
### Step 1 - Frontend minta edit session
|
|
|
|
Frontend hanya mengirim `fileId`.
|
|
|
|
Backend akan:
|
|
|
|
1. load file node
|
|
2. cari takeover asal file
|
|
3. verifikasi user punya akses ke takeover
|
|
4. mint access token baru
|
|
5. balikin Collabora action URL
|
|
|
|
### Step 2 - Frontend buka editor
|
|
|
|
Frontend membuka editor pakai response yang sama seperti flow create-from-template.
|
|
|
|
## Lifecycle Yang Perlu Dipahami Frontend
|
|
|
|
### Saat editor dibuka
|
|
|
|
Backend sudah membuat record file node.
|
|
|
|
Artinya:
|
|
|
|
1. tidak ada lagi langkah create file saat save
|
|
2. Collabora tinggal overwrite file yang sama
|
|
3. `fileId` tetap sama selama sesi itu
|
|
|
|
### Saat user mengetik
|
|
|
|
Collabora akan autosave ke backend melalui WOPI.
|
|
|
|
Frontend tidak perlu memanggil endpoint save sendiri.
|
|
|
|
### Saat user menutup editor
|
|
|
|
Jika Collabora mengirim exit-save:
|
|
|
|
1. backend menandai file `final`
|
|
2. backend attach relasi takeover bila perlu saat create takeover menerima `edited_template_file_ids`
|
|
3. frontend cukup refresh data list / detail
|
|
4. child `fpwb` tetap berada di helicopter files karena row bisnisnya sudah dibuat sejak create-from-template
|
|
|
|
### Jika user menutup tanpa perubahan
|
|
|
|
File tetap berada di status draft dan bisa dibersihkan oleh job cleanup jika stale.
|
|
|
|
## Permission Model
|
|
|
|
### App API
|
|
|
|
App API tetap memakai session auth normal.
|
|
|
|
Frontend harus sudah login sebelum memanggil:
|
|
|
|
1. `POST /api/v1/file-manager/files/create-from-template`
|
|
2. `GET /api/v1/file-manager/files/{fileId}/edit-session`
|
|
|
|
### WOPI API
|
|
|
|
WOPI API memakai:
|
|
|
|
1. `access_token`
|
|
2. proof key validation
|
|
3. permission claim `read` atau `write`
|
|
|
|
Frontend tidak perlu menambahkan permission manual ke WOPI request. Itu sudah ditangani backend saat mint token.
|
|
|
|
## Error Handling Singkat
|
|
|
|
Semua error App API mengikuti envelope `apperrorsx` yang sudah dipakai backend:
|
|
|
|
1. `status`
|
|
2. `code`
|
|
3. `error_code`
|
|
4. `title`
|
|
5. `message`
|
|
|
|
Untuk frontend, patokan utamanya adalah `code` dan `error_code`, bukan hanya HTTP status.
|
|
|
|
Mapping singkat yang dipakai di flow ini:
|
|
|
|
1. `403` App API: `FORBIDDEN_ACCESS` / `USER_UNAUTHORIZED` saat user tidak boleh akses takeover atau file.
|
|
2. `401/403` WOPI: `FILE_MANAGER_WOPI_INVALID_TOKEN`, `FILE_MANAGER_WOPI_INVALID_PROOF`, atau `FILE_MANAGER_WOPI_PERMISSION_DENIED`.
|
|
3. `409` save WOPI: `FILE_MANAGER_WOPI_LOCK_CONFLICT` atau `FILE_MANAGER_WOPI_STALE_TIMESTAMP`.
|
|
4. `422` input invalid: `VALIDATION_ERROR` atau `FILE_MANAGER_WOPI_VALIDATION_FAILED`.
|
|
5. `404` file/takeover missing: `DATA_NOT_FOUND`, `FILE_MANAGER_FILE_NOT_FOUND`, atau `FILE_MANAGER_WOPI_FILE_NOT_FOUND`.
|
|
|
|
Catatan:
|
|
|
|
1. WOPI save conflict kadang tetap mengembalikan body protokol khusus seperti `COOLStatusCode: 1010`.
|
|
2. Untuk WOPI, frontend tetap tidak memanggil endpoint tersebut langsung; yang perlu di-handle adalah response dari backend saat membuka editor.
|
|
|
|
## Hal Yang Harus Dihindari
|
|
|
|
1. Jangan panggil `/wopi/...` langsung dari frontend.
|
|
2. Jangan kirim credential S3 ke browser.
|
|
3. Jangan hardcode file path di frontend.
|
|
4. Jangan anggap token WOPI berlaku lama.
|
|
5. Jangan reuse token file A untuk file B.
|
|
6. Jangan pakai `create-from-template` berulang-ulang hanya untuk refresh editor file yang sama.
|