Skip to main content

Build a Drive-app

Drive-apps enhance how contributors and organizations interact with document models. They create an 'app-like' experience by providing a custom interface for exploring and interacting with the contents of a drive.

What is a Drive-app?

A Drive-app offers a tailored application designed around its document models. Think of a Drive-app as a specialized lens—it offers different ways to visualize, organize, and interact with the data stored within a drive, making it more intuitive and efficient for specific use cases.

Drive-apps are purpose-built

Organizations typically build Drive-apps for specific use cases, often packaging them with a corresponding document model. This allows for customized user experiences, streamlined workflows, and maximized efficiency for contributors.

Drive-apps bridge the gap between raw data and usability, unlocking the full potential of document models within the Powerhouse framework.

Key features of Drive-apps

  • Custom Views & Organization – Drive-apps can present data in formats like Kanban boards, list views, or other structured layouts to suit different workflows.
  • Aggregated Insights – They can provide high-level summaries of important details across document models, enabling quick decision-making.
  • Enhanced Interactivity – Drive-apps can include widgets, data processors, or read models to process and display document data dynamically.

Build a Drive-app

Drive-apps provide custom interfaces for interacting with the contents of a drive. Let's start with a quick overview of the steps for building a Drive-app. We will then apply these steps to create our todo-list Drive-app.

Step 1. Generate the scaffolding code

When using Vetra Studio, Drive-app generation is automatic. Simply create a Drive-app specification document in your Vetra Studio Drive:

  1. Open Vetra Studio (ph vetra --watch)
  2. In your Vetra Studio Drive, click "Add new specification" in the Apps section
  3. Name your Drive-app (e.g., todo-drive-explorer)
  4. Vetra automatically generates the Drive-app template code
Alternatively: Manual generation with ph generate

If you're not using Vetra Studio, use the generate drive editor command to create the basic template structure:

ph generate --drive-editor <drive-app>

Step 2. Update the manifest file

After creating your Drive-app, you need to update its powerhouse.manifest.json file. This file identifies your project and its components within the Powerhouse ecosystem.

Step 3. Customize the Drive-app

Review the generated template and modify it to better suit your document model:

  1. Remove unnecessary files and components
  2. Add custom views specific to your data model
  3. Implement specialized interactions for your use case

About the Drive-app template

The default template provides a solid foundation. It contains:

  • A tree structure navigation panel
  • Basic file/folder operations
  • Standard layout components

But the real power comes from tailoring the interface to your specific document models. Now, let's implement a specific example for the to-do list we've been working on throughout this guide.

Implementation example: todo-list Drive-app

This example demonstrates how to create a todo-list Drive-app application using the Powerhouse platform. The application allows users to create and manage to-do lists with a visual progress indicator.

Heads-up!

If you've been following the Mastery Track, you can continue with the to-do list document model and Powerhouse project you've created. For more details, you can refer to the Document Model Creation guide.

If not, you can follow the shortened guide below to prepare your project for this tutorial.

Prepare your Powerhouse Project to create a custom drive

1. Create a To-do document model:

  • Initialize a new project with ph init and give it a project name.

  • Start by running Vetra Studio locally with ph vetra --watch

  • Download the todolist.phdm.zip file from the todo-demo-package GitHub repository.

  • Drop the downloaded file in the Vetra Studio drive. You'll find it under document models. Vetra should now automatically generate the necessary code for your project

2. Add the reducer code:

  • Copy the code from base-operations.ts
  • Paste it into document-models/to-do/src/reducers/base-operations.ts

3. Generate a document editor:

In Vetra Studio, create an Editor specification:

  1. Click "Add new specification" in the Editors section
  2. Select the TodoList document model
  3. Name your editor TodoList
  4. Vetra automatically generates the editor template
Alternatively: Manual generation
ph generate --editor TodoList --document-types powerhouse/todo-list

4. Add the editor code:

  • Copy the code from editor.tsx
  • Paste it into editors/to-do-list/editor.tsx
Alternatively: Use Connect instead of Vetra Studio

You can also start by running Connect locally with ph connect instead of Vetra Studio. The workflow is the same, with Connect providing a similar development environment.

Generate the Drive-app

1. Generate a Drive-app:

With Vetra Studio running (ph vetra --watch), create a Drive-app specification:

  1. In your Vetra Studio Drive, click "Add new specification" in the Apps section
  2. Name your Drive-app todo-drive-explorer
  3. Vetra automatically generates the Drive-app template in editors/todo-drive-explorer/
Alternatively: Manual generation with ph generate
ph generate --drive-editor todo-drive-explorer

2. Update the powerhouse.manifest.json file:

  • The manifest file contains metadata for your package that is displayed when other users install it. Update the manifest to register your new Drive-app:
{
"name": "To-do List Package",
"description": "A simple todo-list with a dedicated Drive-app",
"category": "Productivity",
"publisher": {
"name": "Powerhouse",
"url": "https://www.powerhouse.inc/"
},
"documentModels": [
{
"id": "to-do-list",
"name": "To-do List"
}
],
"editors": [
{
"id": "to-do-list-editor",
"name": "To-do List Editor",
"documentTypes": ["todo-list"]
}
],
"apps": [
{
"id": "todo-drive-explorer",
"name": "To-do drive-app",
"driveEditor": "todo-drive-explorer"
}
],
"subgraphs": [],
"importScripts": []
}

3. Remove Unnecessary Default Components:

  • First, let's remove some default template files that we won't need for this specific demo. If you want to see what the default template looks like before removing files, you can run ph connect at any time.
rm -rf editors/todo-drive-explorer/hooks
rm -rf editors/todo-drive-explorer/components/FileItemsGrid.tsx
rm -rf editors/todo-drive-explorer/components/FolderItemsGrid.tsx
rm -rf editors/todo-drive-explorer/components/FolderTree.tsx

4. Create custom components for your Drive-app:

  • Next, create the following files. These will define the data types for our todo-list items and provide the custom React components for our Drive-app.
Create editors/todo-drive-explorer/types/todo.ts

This file defines the TypeScript type ToDoState. It specifies the shape of todo-list document data within the Drive-app, combining the document's revision information with its global state. This ensures that our components work with a predictable and strongly-typed data structure.

import { type ToDoListDocument} from "../../../document-models/to-do-list/index.js"

export type ToDoState = {
documentType: string;
revision: {
global: number;
local: number;
};
global: ToDoListDocument["state"]["global"];
};
Create editors/todo-drive-explorer/components/ProgressBar.tsx

This is a simple React component that renders a visual progress bar. It takes a value and max number to calculate the percentage of completed tasks. It also displays the percentage and has a special state for when there are no tasks.

 import type { FC } from 'react';

interface ProgressBarProps {
value: number;
max: number;
}

export const ProgressBar: FC<ProgressBarProps> = ({ value, max }) => {
if (max === 0) {
return (
<div className="w-full bg-gray-200 rounded-full h-4">
<div className="bg-gray-300 h-4 rounded-full text-xs text-center text-gray-500">
No tasks
</div>
</div>
);
}

const percentage = Math.min(100, (value / max) * 100);

return (
<div className="w-full bg-gray-200 rounded-full h-4 relative">
<div
className="bg-blue-500 h-4 rounded-full transition-all duration-300"
style={{ width: `${percentage}%` }}
/>
<div className="absolute inset-0 flex items-center justify-center text-xs font-medium text-black">
{Math.round(percentage)}%
</div>
</div>
);
};
Update editors/todo-drive-explorer/components/DriveExplorer.tsx

This is the main component of our Drive-app. It fetches all powerhouse/todo-list documents from the drive, displays them in a table with their progress, and allows a user to click on a document to open it in the EditorContainer. It also includes a button to create new documents.

import { useCallback, useState, useRef, useEffect, useMemo } from "react";
import type { FileNode, GetDocumentOptions, Node } from "document-drive";
import { EditorContainer, EditorContainerProps } from "./EditorContainer.js";
import type { DocumentModelModule } from "document-model";
import { CreateDocumentModal } from "@powerhousedao/design-system";
import { CreateDocument } from "./CreateDocument.js";
import { type DriveEditorContext, useDriveContext } from "@powerhousedao/reactor-browser";
import { ProgressBar } from "./ProgressBar.js";

import { type ToDoState } from "../types/todo.js"

interface DriveExplorerProps {
driveId: string;
nodes: Node[];
onAddFolder: (name: string, parentFolder?: string) => void;
onDeleteNode: (nodeId: string) => void;
renameNode: (nodeId: string, name: string) => void;
onCopyNode: (nodeId: string, targetName: string, parentId?: string) => void;
context: DriveEditorContext;
}

export function DriveExplorer({
driveId,
nodes,
context,
}: DriveExplorerProps) {
const { getDocumentRevision } = context;

const [activeDocumentId, setActiveDocumentId] = useState<
string | undefined
>();
const [openModal, setOpenModal] = useState(false);
const selectedDocumentModel = useRef<DocumentModelModule | null>(null);
const { addDocument, documentModels, useDriveDocumentStates } = useDriveContext();

const [state, fetchDocuments] = useDriveDocumentStates({ driveId });

useEffect(() => {
fetchDocuments(driveId).catch(console.error);
}, [activeDocumentId]);

const { todoNodes } = useMemo(() => {
return Object.keys(state).reduce(
(acc, curr) => {
const document = state[curr];
if (document.documentType.startsWith("powerhouse/todo")) {
acc.todoNodes[curr] = document as ToDoState;
}

return acc;
},
{
todoNodes: {} as Record<string, ToDoState>,
},
);
}, [state]);


const handleEditorClose = useCallback(() => {
setActiveDocumentId(undefined);
}, []);

const onCreateDocument = useCallback(
async (fileName: string) => {
setOpenModal(false);

const documentModel = selectedDocumentModel.current;
if (!documentModel) return;

const node = await addDocument(
driveId,
fileName,
documentModel.documentModel.id,
);

selectedDocumentModel.current = null;
setActiveDocumentId(node.id);
},
[addDocument, driveId],
);

const onSelectDocumentModel = useCallback(
(documentModel: DocumentModelModule) => {
selectedDocumentModel.current = documentModel;
setOpenModal(true);
},
[],
);

const onGetDocumentRevision = useCallback(
(options?: GetDocumentOptions) => {
if (!activeDocumentId) return;
return getDocumentRevision?.(activeDocumentId, options);
},
[getDocumentRevision, activeDocumentId],
);

const filteredDocumentModels = documentModels;


const fileNodes = nodes.filter((node) => node.kind === "file") as FileNode[];
// Get the active document info from nodes
const activeDocument = activeDocumentId
? fileNodes.find((file) => file.id === activeDocumentId)
: undefined;

const documentModelModule = activeDocument
? context.getDocumentModelModule(activeDocument.documentType)
: null;

const editorModule = activeDocument
? context.getEditor(activeDocument.documentType)
: null;


return (
<div className="flex h-full">
{/* Main Content */}
<div className="flex-1 p-4 overflow-y-auto">
{activeDocument && documentModelModule && editorModule ? (
<EditorContainer
context={{
...context,
getDocumentRevision: onGetDocumentRevision,
}}
documentId={activeDocumentId!}
documentType={activeDocument.documentType}
driveId={driveId}
onClose={handleEditorClose}
title={activeDocument.name}
documentModelModule={documentModelModule}
editorModule={editorModule}
/>
) : (
<>
<h2 className="text-lg font-semibold mb-4">ToDos:</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Document ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Document Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tasks</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Completed</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Progress</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{Object.entries(todoNodes).map(([documentId, todoNode]) => (
<tr key={documentId} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div
onClick={() => setActiveDocumentId(documentId)}
className="text-blue-600 hover:text-blue-800 cursor-pointer"
>
{documentId}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{todoNode.documentType}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{todoNode.global.stats.total}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{todoNode.global.stats.checked}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="w-32">
<ProgressBar
value={todoNode.global.stats.checked}
max={todoNode.global.stats.total}
/>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>

{/* Create Document Section */}
<CreateDocument
createDocument={onSelectDocumentModel}
documentModels={filteredDocumentModels}
/>
</>
)}
</div>

{/* Create Document Modal */}
<CreateDocumentModal
onContinue={onCreateDocument}
onOpenChange={(open) => setOpenModal(open)}
open={openModal}
/>
</div>
);
}
Update editors/todo-drive-explorer/components/EditorContainer.tsx

This component acts as a wrapper for the document editor. When a user selects a document in DriveExplorer.tsx, this component mounts the appropriate editor (to-do-list editor in this case) and provides it with the necessary context and properties to function. It also renders the DocumentToolbar which provides actions like closing, exporting, and viewing revision history.

import {
useDriveContext,
exportDocument,
type User,
type DriveEditorContext,
} from "@powerhousedao/reactor-browser";
import {
documentModelDocumentModelModule,
type DocumentModelModule,
type EditorContext,
type EditorProps,
type PHDocument,
type EditorModule,
type Operation,
} from "document-model";
import { useTimelineItems, getRevisionFromDate } from "@powerhousedao/common";
import {
DocumentToolbar,
RevisionHistory,
DefaultEditorLoader,
generateLargeTimeline,
type TimelineItem,
} from "@powerhousedao/design-system";
import { useState, Suspense, type FC, useCallback } from "react";

export interface EditorContainerProps {
driveId: string;
documentId: string;
documentType: string;
onClose: () => void;
title: string;
context: Omit<DriveEditorContext, "getDocumentRevision"> &
Pick<EditorContext, "getDocumentRevision">;
documentModelModule: DocumentModelModule<PHDocument>;
editorModule: EditorModule;
}

export const EditorContainer: React.FC<EditorContainerProps> = (props) => {
const { driveId, documentId, documentType, onClose, title, context, documentModelModule, editorModule } = props;

const [selectedTimelineItem, setSelectedTimelineItem] = useState<TimelineItem | null>(null);
const [showRevisionHistory, setShowRevisionHistory] = useState(false);
const { useDocumentEditorProps } = useDriveContext();
const user = context.user as User | undefined;
const timelineItems = useTimelineItems(documentId);

const { dispatch, error, document } = useDocumentEditorProps({
documentId,
documentType,
driveId,
documentModelModule,
user,
});

const onExport = useCallback(async () => {
if (document) {
const ext = documentModelModule.documentModel.extension;
await exportDocument(document, title, ext);
}
}, [document?.revision.global, document?.revision.local]);

const loadingContent = (
<div className="flex-1 flex justify-center items-center h-full">
<DefaultEditorLoader />
</div>
);

if (!document) return loadingContent;

const moduleWithComponent = editorModule as EditorModule<PHDocument>;
const EditorComponent = moduleWithComponent.Component;

return showRevisionHistory ? (
<RevisionHistory
documentId={documentId}
documentTitle={title}
globalOperations={document.operations.global}
key={documentId}
localOperations={document.operations.local}
onClose={() => setShowRevisionHistory(false)}
/>
) : (
<Suspense fallback={loadingContent}>
<DocumentToolbar
onClose={onClose}
onExport={onExport}
onShowRevisionHistory={() => setShowRevisionHistory(true)}
onSwitchboardLinkClick={() => {}}
title={title}
timelineButtonVisible
timelineItems={timelineItems.data}
onTimelineItemClick={setSelectedTimelineItem}
/>
<EditorComponent
context={{
...context,
readMode: !!selectedTimelineItem,
selectedTimelineRevision: getRevisionFromDate(
selectedTimelineItem?.startDate,
selectedTimelineItem?.endDate,
document.operations.global,
),
}}
dispatch={dispatch}
document={document}
error={error}
/>
</Suspense>
);
};
  • In case you are getting stuck and want to verify your progress with the reference repository you can find the example repository of the Todo-demo-package here

3. Run the application:

  • With the code for our Drive-app in place, it's time to see it in action. If you've been running Vetra Studio with ph vetra --watch, your Drive-app is already available. Otherwise, run Connect:
ph connect

Now it's your turn!

Start building your own Drive-apps or experiences. Congratulations on completing this tutorial! You've successfully built a custom drive explorer, enhancing the way users interact with document models.

Now, take a moment to think about the possibilities!

  • What unique Drive Experiences could you create for your own projects?
  • How can you tailor interfaces and streamline workflows to unlock the full potential of your document models?

The Powerhouse platform provides the tools. It's time to start building!