From 0f338720a36768c3f624c4024c03a7b6f957a88c Mon Sep 17 00:00:00 2001 From: pikann22 Date: Wed, 15 Jul 2026 06:40:55 +0000 Subject: [PATCH] feat: update project version, enhance PluginApiClient options, and add new extension point interfaces --- .github/workflows/cd.yml | 11 ++++++++--- package.json | 2 +- src/api-client.ts | 27 ++++++++++++++++++--------- src/extension-points.ts | 34 +++++++++++++++++++++++++++++++++- src/index.ts | 2 ++ 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 20d97b5..c0651a7 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -48,12 +48,17 @@ jobs: - name: Setup Node.js with npm registry uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" registry-url: "https://registry.npmjs.org" scope: "@paca-ai" - - name: Upgrade npm - run: npm install -g npm@latest + # npm 12.0.0 has a packaging bug: libnpmpublish still requires + # `sigstore`, but the published 12.0.0 tarball is missing that + # module, so `npm publish --provenance` crashes with "Cannot find + # module 'sigstore'". Pin to the 11.x line until a patched 12.x + # ships. See https://github.com/npm/cli/issues/9722. + - name: Pin npm to a working version + run: npm install -g npm@11 # For prerelease versions (e.g. 1.0.0-alpha.1), npm requires an explicit # --tag to avoid accidentally tagging as "latest". diff --git a/package.json b/package.json index 6e34dad..cc2dd7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paca-ai/plugin-sdk-react", - "version": "0.1.0", + "version": "0.3.0", "description": "Paca Plugin SDK — frontend helpers for building Paca micro-frontend plugins", "type": "module", "publishConfig": { diff --git a/src/api-client.ts b/src/api-client.ts index e6a5de1..53c47de 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -27,8 +27,13 @@ interface SuccessEnvelope { export interface PluginApiClientOptions { /** Base URL of the paca API, e.g. "https://app.paca.dev/api/v1". */ baseUrl: string; - /** Current project ID (injected by the host). */ - projectId: string; + /** + * Current project ID (injected by the host). Omit for admin/global-scope + * pages (e.g. components registered at the `admin.page` extension point) + * where there is no current project — `projectId` is then `""` and + * `listTasks`/`getTask`/`getProject`/`listMembers` must not be called. + */ + projectId?: string; /** Axios-compatible function for authenticated fetches (injected by host). */ fetch: (url: string, init?: RequestInit) => Promise; } @@ -46,13 +51,18 @@ export class PluginApiClient { constructor(opts: PluginApiClientOptions) { this.baseUrl = opts.baseUrl.replace(/\/$/, ""); - this.projectId = opts.projectId; + this.projectId = opts.projectId ?? ""; this._fetch = opts.fetch; } // ── Core read-only helpers ────────────────────────────────────────────── - /** List tasks for the current project with optional filters. */ + /** + * List tasks for the current project with optional filters. + * Defaults to the host's max page size (200) when the caller doesn't + * specify one, since most callers use this for an unfiltered "get the + * project's tasks" lookup and the host otherwise defaults to only 20. + */ async listTasks(filters: TaskFilters = {}): Promise { const params = new URLSearchParams(); if (filters.status_ids?.length) @@ -63,12 +73,12 @@ export class PluginApiClient { if (filters.parent_task_id) params.set("parent_task_id", filters.parent_task_id); if (filters.page) params.set("page", String(filters.page)); - if (filters.page_size) params.set("page_size", String(filters.page_size)); + params.set("page_size", String(filters.page_size ?? 200)); const qs = params.toString(); const url = `${this.baseUrl}/projects/${this.projectId}/tasks${qs ? `?${qs}` : ""}`; - const envelope = await this._get<{ tasks: TaskSummary[] }>(url); - return envelope.tasks; + const envelope = await this._get<{ items: TaskSummary[] }>(url); + return envelope.items; } /** Get a single task by ID. */ @@ -87,10 +97,9 @@ export class PluginApiClient { /** List members of the current project. */ async listMembers(): Promise { - const envelope = await this._get<{ members: ProjectMember[] }>( + return this._get( `${this.baseUrl}/projects/${this.projectId}/members`, ); - return envelope.members; } // ── Plugin route helpers ──────────────────────────────────────────────── diff --git a/src/extension-points.ts b/src/extension-points.ts index fd22347..c79e014 100644 --- a/src/extension-points.ts +++ b/src/extension-points.ts @@ -11,6 +11,8 @@ * - task.detail.section * - project.settings.tab * - view + * - project.page (full-page view routed from a project sidebar nav item) + * - admin.page (full-page view routed from the admin sidebar nav item) */ import type { PluginApiClient } from "./api-client"; @@ -90,6 +92,34 @@ export interface ViewExtensionProps extends BaseExtensionProps { viewConfig?: Record; } +// ── project.page ────────────────────────────────────────────────────────────── + +/** + * Props for components registered at `project.page`. + * Rendered full-bleed (outside the settings-tab layout) at a dedicated route, + * reached via a nav item the plugin registers in the project sidebar. Use this + * for a plugin feature that deserves its own page rather than a settings tab + * or a small sidebar fragment — e.g. a project-wide time-tracking view. + */ +export interface ProjectPageProps extends BaseExtensionProps { + /** The project this page belongs to. */ + projectId: string; +} + +// ── admin.page ──────────────────────────────────────────────────────────────── + +/** + * Props for components registered at `admin.page`. + * Rendered full-bleed at a dedicated route under the host's admin section, + * reached via a nav item the plugin registers in the admin sidebar. Only + * visible to users with the `users.write` global permission (same gate as + * the built-in admin pages). Use this for cross-project / instance-wide + * plugin views — e.g. total logged time across all projects. The `api` + * client injected here has no `projectId` (global scope); use + * `api.pluginGet(pluginId, "/some-global-path")` for admin-scoped routes. + */ +export interface AdminPageProps extends BaseExtensionProps {} + // ── Union helper ────────────────────────────────────────────────────────────── /** Union of all extension point prop types. */ @@ -98,4 +128,6 @@ export type ExtensionPointProps = | SidebarProjectSectionProps | TaskDetailSectionProps | ProjectSettingsTabProps - | ViewExtensionProps; + | ViewExtensionProps + | ProjectPageProps + | AdminPageProps; diff --git a/src/index.ts b/src/index.ts index 6ebf17e..572f152 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ export { PluginApiClient } from "./api-client"; export type { BaseExtensionProps, ExtensionPointProps, + AdminPageProps, + ProjectPageProps, ProjectSettingsTabProps, SidebarGeneralSectionProps, SidebarProjectSectionProps,