@shiftengineering/folio
TypeScript icon, indicating that this package has built-in type declarations

0.1.32 • Public • Published

@shiftengineering/folio

A React component library for embedding and interacting with Folio documents, projects, and files in your application.

Installation

# Using npm
npm install @shiftengineering/folio

# Using yarn
yarn add @shiftengineering/folio

# Using pnpm
pnpm add @shiftengineering/folio

Usage

This package exports three main features:

  1. FolioProvider - Context provider for Folio connections
  2. FolioEmbed - React component to embed Folio in an iframe
  3. React hooks for interacting with Folio data:
    • useFolioProjects - Get projects data
    • useFolioFiles - Get files for a project
    • useAddFolioProject - Create new projects
    • useAddFolioFiles - Add files to a project
    • useAddFolioDirectoriesWithFiles - Add directories with files to a project
    • useFolioUserMetadata - Get and update user metadata

Secure Token Handling

By default, the component uses a secure token passing mechanism via postMessage instead of passing the JWT token as a URL query parameter. This ensures your token is not visible in network logs or browser history.

The token is passed securely as follows:

  1. The iframe loads without the token in the URL
  2. When the iframe is ready, it requests the token from the parent via postMessage
  3. The parent application responds with the token, which is then used for API requests

If you need backward compatibility with older versions, you can set the passTokenInQueryParam property to true on the FolioProvider:

<FolioProvider
  host="http://your-folio-server.com"
  port={5174}
  token={token}
  passTokenInQueryParam={true} // Legacy mode: passes token in URL (less secure)
>
  <App />
</FolioProvider>

Basic Setup

First, wrap your application with the FolioProvider:

import { FolioProvider } from "@shiftengineering/folio";
import App from "./App";

// The token must be a valid JWT that the Folio backend is configured to accept
const token = "your-jwt-auth-token";

// Optional user metadata to personalize AI responses
const userMetadata = {
  role: "Sales Representative",
  industry: "Healthcare",
};

ReactDOM.render(
  <FolioProvider
    host="http://your-folio-server.com"
    port={5174}
    token={token}
    userMetadata={userMetadata}
  >
    <App />
  </FolioProvider>,
  document.getElementById("root"),
);

Embedding Folio

Use the FolioEmbed component to embed Folio in your application:

import { FolioEmbed } from "@shiftengineering/folio";

function MyFolioPage() {
  return (
    <div className="folio-page">
      <h1>My Folio Documents</h1>
      <FolioEmbed
        width="100%"
        height="800px"
        className="my-custom-class"
        style={{ border: "1px solid #ccc" }}
      />
    </div>
  );
}

Working with Folio Data

Use the Folio hooks to get and manipulate Folio data:

import {
  useFolioProjects,
  useFolioFiles,
  useAddFolioProject,
  useAddFolioFiles,
  useAddFolioDirectoriesWithFiles,
  useFolioUserMetadata,
  type DirectoryEntry,
  type MetadataValue,
} from "@shiftengineering/folio";
import { useState } from "react";

function FolioProjectManager() {
  const [selectedProjectId, setSelectedProjectId] = useState(null);

  // Get projects with loading and error states
  const {
    projects,
    isLoading: isProjectsLoading,
    error: projectsError,
  } = useFolioProjects();

  // Get files for the selected project
  const { files, isLoading: isFilesLoading } = useFolioFiles(selectedProjectId);

  // Get and update user metadata
  const {
    metadata: userMetadata,
    updateMetadata,
    isLoading: isMetadataLoading
  } = useFolioUserMetadata();

  // Add a new project
  const { addProject, isAdding: isCreatingProject } = useAddFolioProject();

  // Add files to a project
  const { addFiles, isAdding: isAddingFiles } =
    useAddFolioFiles(selectedProjectId);

  // Add directories with files to a project
  const { addDirectoriesWithFiles, isAdding: isAddingDirectory } =
    useAddFolioDirectoriesWithFiles(selectedProjectId);

  const handleCreateProject = () => {
    addProject("My New Project");
  };

  const handleUpdateUserMetadata = () => {
    updateMetadata({
      role: "Project Manager",
      industry: "Finance",
      interestedIn: "State contracts"
    });
  };

  const handleAddFile = () => {
    addFiles([{
      blobUrl: "/path/to/file.pdf",
      name: "My Document.pdf",
      userProvidedId: "doc-123" // Required unique identifier for this file
    }]);
  };

  const handleAddSingleDirectory = () => {
    // Create metadata with nested structure
    const metadata = {
      category: "reports",
      details: {
        owner: "John Doe",
        department: "Finance",
        tags: ["important", "quarterly"],
      },
      status: {
        reviewed: true,
        approvalDate: "2023-10-15"
      }
    };

    const directoryEntry: DirectoryEntry = {
      directoryName: "My Documents",
      directoryMetadata: metadata,
      files: [
        {
          blobUrl: "/path/to/file1.pdf",
          name: "Document 1.pdf",
          userProvidedId: "doc-456" // Required unique identifier for this file
        },
        {
          blobUrl: "/path/to/file2.pdf",
          name: "Document 2.pdf",
          userProvidedId: "doc-789" // Required unique identifier for this file
        },
      ],
    };

    addDirectoriesWithFiles(directoryEntry);
  };

  const handleAddMultipleDirectories = () => {
    // Create metadata for each directory with nested structures
    const metadata1 = {
      category: "reports",
      details: {
        owner: "Jane Smith",
        department: "Accounting",
        tags: ["quarterly", "financial"]
      }
    };

    const metadata2 = {
      category: "contracts",
      details: {
        owner: "Legal Team",
        priority: "high",
        clients: ["Acme Inc", "Globex Corp"]
      },
      approvalChain: {
        legalApproved: true,
        executiveApproved: false
      }
    };

    const directories: DirectoryEntry[] = [
      {
        directoryName: "Reports",
        directoryMetadata: metadata1,
        files: [
          {
            blobUrl: "/path/to/report1.pdf",
            name: "Report 1.pdf",
            userProvidedId: "report-1" // Required unique identifier for this file
          },
          {
            blobUrl: "/path/to/report2.pdf",
            name: "Report 2.pdf",
            userProvidedId: "report-2" // Required unique identifier for this file
          },
        ],
      },
      {
        directoryName: "Contracts",
        directoryMetadata: metadata2,
        files: [
          {
            blobUrl: "/path/to/contract1.pdf",
            name: "Contract 1.pdf",
            userProvidedId: "contract-1" // Required unique identifier for this file
          },
          {
            blobUrl: "/path/to/contract2.pdf",
            name: "Contract 2.pdf",
            userProvidedId: "contract-2" // Required unique identifier for this file
          },
        ],
      },
    ];

    addDirectoriesWithFiles(directories);
  };

  if (isProjectsLoading) return <div>Loading projects...</div>;
  if (projectsError) return <div>Error: {projectsError.message}</div>;

  return (
    <div>
      <button onClick={handleCreateProject} disabled={isCreatingProject}>
        {isCreatingProject ? "Creating..." : "Create New Project"}
      </button>

      <h2>Your Projects</h2>
      <ul>
        {projects.map((project) => (
          <li
            key={project.id}
            onClick={() => setSelectedProjectId(project.id)}
            style={{
              fontWeight: project.id === selectedProjectId ? "bold" : "normal",
            }}
          >
            {project.name}
          </li>
        ))}
      </ul>

      {selectedProjectId && (
        <>
          <h2>Project Files</h2>
          <button onClick={handleAddFile} disabled={isAddingFiles}>
            {isAddingFiles ? "Adding..." : "Add File"}
          </button>
          <button onClick={handleAddSingleDirectory} disabled={isAddingDirectory}>
            {isAddingDirectory ? "Adding..." : "Add Directory with Files"}
          </button>
          <button onClick={handleAddMultipleDirectories} disabled={isAddingDirectory}>
            {isAddingDirectory ? "Adding..." : "Add Multiple Directories"}
          </button>

          {isFilesLoading ? (
            <div>Loading files...</div>
          ) : (
            <ul>
              {files.map((file) => (
                <li key={file.id}>
                  {file.name}
                  {file.userProvidedId && <small> (ID: {file.userProvidedId})</small>}
                </li>
              ))}
            </ul>
          )}
        </>
      )}
    </div>
  );
}

Google Analytics 4 Integration

Folio supports analytics event tracking that can be used with Google Analytics 4 or any other analytics provider. This feature enables tracking key user interactions and provides visibility into how users engage with the application.

Configuration

For Google Analytics 4

To enable built-in Google Analytics 4 tracking, add the measurement ID to your environment variables provided to the docker container:

VITE_GA4_MEASUREMENT_ID=G-XXXXXXXXXX

When this environment variable is present, Folio will automatically initialize GA4 and send events to Google Analytics.

For Any Analytics Provider

Important: Even if you don't configure GA4, you can still capture all analytics events by providing the onAnalyticsEvent callback to the FolioProvider. This gives you complete flexibility to use any analytics provider of your choice.

Events Tracked

The following events are tracked automatically:

  1. page_view - When a user navigates to a new page
  2. chat_sent - When a user sends a chat message (includes the query)
  3. highlight - When a user creates a highlight (includes file path and selection length)
  4. add_to_chat - When a user adds content to chat (includes file path and snippet size)
  5. extract - When content is extracted (includes file path and extractor type)
  6. switch_project - When a user switches between projects
  7. file_view - When a user views a file

Analytics Event Structure

All analytics events follow this structure:

export type AnalyticsEvent =
  | { name: "page_view"; data: { pathname: string; projectId?: string } }
  | { name: "chat_sent"; data: { query: string } }
  | { name: "highlight"; data: { filePath: string; selectionLength: number } }
  | { name: "add_to_chat"; data: { filePath: string; snippetSize: number } }
  | { name: "extract"; data: { filePath: string; extractor: string } }
  | {
      name: "switch_project";
      data: { fromProjectId: string; toProjectId: string };
    }
  | { name: "file_view"; data: { filePath: string } };

Each event has:

  • A name property identifying the event type
  • A data object with event-specific parameters

Host Integration

If you're embedding Folio in your application, you can access the analytics event stream by providing the onAnalyticsEvent callback to the FolioProvider:

import { FolioProvider, AnalyticsEvent } from "@shiftengineering/folio";

function YourApp() {
  const handleAnalyticsEvent = (event: AnalyticsEvent) => {
    // Forward to your own analytics system or process the data
    console.log(`Folio event: ${event.name}`, event.data);

    // Example: Send to Google Analytics
    if (window.gtag) {
      window.gtag('event', event.name, event.data);
    }

    // Example: Send to Mixpanel
    if (window.mixpanel) {
      window.mixpanel.track(event.name, event.data);
    }

    // Example: Send to custom analytics endpoint
    fetch('https://your-analytics-api.com/track', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event)
    });
  };

  return (
    <FolioProvider
      host="http://your-folio-server.com"
      port={5174}
      token="your-auth-token"
      onAnalyticsEvent={handleAnalyticsEvent}
    >
      <YourAppContent />
    </FolioProvider>
  );
}

Alternatively, you can listen to the raw custom event directly:

window.addEventListener("folio-analytics", (event) => {
  // Access the event data from event.detail
  const { name, data } = event.detail;

  // Forward to your own analytics system or process the data
  console.log(`Folio event: ${name}`, data);
});

Both approaches allow host applications to consume the same events regardless of whether GA4 is configured, enabling integration with any analytics service or custom tracking solution.

API Reference

FolioProvider

Context provider that manages Folio application connection settings.

Prop Type Default Description
host string 'http://localhost' Host for the Folio API and iframe
port number 5174 Port for the Folio API and iframe
token string - JWT authentication token that the Folio backend is configured to accept
userMetadata Record<string, MetadataValue> - Optional metadata for the current user that will be used to personalize AI responses
onAnalyticsEvent (event: AnalyticsEvent) => void - Optional callback for handling analytics events from Folio
passTokenInQueryParam boolean false Whether to pass the token in URL (legacy, less secure) instead of using postMessage

FolioEmbed

React component to embed Folio in an iframe.

Prop Type Default Description
width string | number '100%' Width of the iframe
height string | number '100vh' Height of the iframe
allow string 'camera; microphone; clipboard-read; clipboard-write; fullscreen' Allow attributes for the iframe
style object {} Additional styles for the iframe container
className string '' Additional class names for the iframe container
iframeProps object {} Additional props to pass to the iframe

Hooks

useFolioProjects()

Hook for getting all projects for the current user.

Return Property Type Description
projects FolioProject[] Array of projects
isLoading boolean Whether projects are being loaded
isError boolean Whether an error occurred
error Error | null Error object if an error occurred
refetch () => Promise<...> Function to manually refetch projects

useFolioFiles(projectId?: number)

Hook for getting all files for a specific project.

Return Property Type Description
files FolioFile[] Array of files
isLoading boolean Whether files are being loaded
isError boolean Whether an error occurred
error Error | null Error object if an error occurred
refetch () => Promise<...> Function to manually refetch files

useAddFolioProject()

Hook for adding a new project.

Return Property Type Description
addProject (name: string) => void Function to add a project
addProjectAsync (name: string) => Promise<FolioProject> Async version returning a promise
isAdding boolean Whether a project is being added
isError boolean Whether an error occurred
error Error | null Error object if an error occurred
newProject FolioProject | undefined The newly created project if available

useAddFolioFiles(projectId?: number)

Hook for adding files to a project. Files are always created at the root level (parentId = null) and are not directories.

Return Property Type Description
addFiles (files: { blobUrl: string; name: string; userProvidedId: string }[]) => void Function to add files
addFilesAsync (files: { blobUrl: string; name: string; userProvidedId: string }[]) => Promise<FolioFile[]> Async version returning a promise
isAdding boolean Whether files are being added
isError boolean Whether an error occurred
error Error | null Error object if an error occurred
newFiles FolioFile[] | undefined The newly added files if available

useAddFolioDirectoriesWithFiles(projectId?: number)

Hook for adding one or more directories with files to a project. Directory names must be unique at the root level (duplicates will be silently skipped with a console warning).

Return Property Type Description
addDirectoriesWithFiles (params: DirectoryEntry | DirectoryEntry[]) => void Function to add one or more directories with files
addDirectoriesWithFilesAsync (params: DirectoryEntry | DirectoryEntry[]) => Promise<{ directory: FolioFile | null; files: FolioFile[] } | Array<{ directory: FolioFile | null; files: FolioFile[] }>> Async version returning a promise. Returns a single result when given a single directory, or an array of results when given multiple directories
isAdding boolean Whether the directories and files are being added
isError boolean Whether an error occurred
error Error | null Error object if an error occurred
result { directory: FolioFile | null; files: FolioFile[] } | Array<{ directory: FolioFile | null; files: FolioFile[] }> | undefined The newly added directories and files. If a directory is null in a result, it means a directory with that name already existed

useFolioUserMetadata()

Hook for retrieving and updating the current user's metadata.

Return Property Type Description
metadata string | null The user's metadata as a string, or null
isLoading boolean Whether metadata is being loaded
isError boolean Whether an error occurred
error Error | null Error object if an error occurred
updateMetadata (metadata: Record<string, MetadataValue>) => void Function to update user metadata
updateMetadataAsync (metadata: Record<string, MetadataValue>) => Promise<void> Async version returning a promise
isUpdating boolean Whether metadata is being updated
refetch () => Promise<...> Function to manually refetch metadata

Types

The library exports these TypeScript types:

Type Description
FolioFile Represents a file in Folio. Contains properties: id, name, blobUrl, parentId (null for root items), isDirectory (boolean), userProvidedId (string), createdAt (Date), and updatedAt (Date)
FolioProject Represents a project in Folio. Contains properties: id, name, createdAt (Date), and updatedAt (Date)
MetadataValue Represents metadata values that can be nested. Can be a string, number, boolean, null, object, or array of these types. Used for both directory metadata and user metadata.
DirectoryEntry Represents a directory with metadata and files to be added to Folio. Contains properties: directoryName, directoryMetadata (now supports nested objects), and files
FolioFileInput Input type for adding files to Folio. Contains properties: blobUrl, name, and userProvidedId (required for deduplication)
AnalyticsEvent A union type for analytics events sent by Folio. Each event has a name property (like "page_view" or "file_view") and a data object with event-specific parameters. See the Analytics Event Structure section for details on all event types.
FolioEmbedProps Props for the FolioEmbed component
FolioProviderProps Props for the FolioProvider component
FolioClient Interface for the client that interacts with the Folio API

License

Readme

Keywords

none

Package Sidebar

Install

npm i @shiftengineering/folio

Weekly Downloads

152

Version

0.1.32

License

none

Unpacked Size

271 kB

Total Files

5

Last publish

Collaborators

  • peter.bull
  • zakinator123
  • shift-eric
  • seth.wright
  • heybluez