@powerhousedao/academy 5.1.0-dev.0 → 5.1.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/blog/BeyondCommunication-ABlueprintForDevelopment.md +2 -1
- package/blog/TheChallengeOfChange.md +1 -0
- package/docs/academy/01-GetStarted/00-ExploreDemoPackage.mdx +24 -27
- package/docs/academy/01-GetStarted/01-CreateNewPowerhouseProject.md +118 -9
- package/docs/academy/01-GetStarted/02-DefineToDoListDocumentModel.md +110 -28
- package/docs/academy/01-GetStarted/03-ImplementOperationReducers.md +191 -145
- package/docs/academy/01-GetStarted/04-WriteDocumentModelTests.md +378 -0
- package/docs/academy/01-GetStarted/05-BuildToDoListEditor.md +560 -0
- package/docs/academy/01-GetStarted/_04-BuildToDoListEditor +1 -1
- package/docs/academy/02-MasteryTrack/01-BuilderEnvironment/03-BuilderTools.md +2 -2
- package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/05-VetraStudio.md +48 -6
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/02-SpecifyTheStateSchema.md +75 -44
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/03-SpecifyDocumentOperations.md +28 -22
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/04-UseTheDocumentModelGenerator.md +28 -31
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/05-ImplementDocumentReducers.md +211 -206
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/06-ImplementDocumentModelTests.md +176 -62
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/07-ExampleToDoListRepository.md +21 -0
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/01-BuildingDocumentEditors.md +309 -319
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/00-DocumentToolbar.mdx +4 -0
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/01-OperationHistory.md +4 -0
- package/docs/academy/02-MasteryTrack/05-Launch/04-ConfigureEnvironment.md +1 -1
- package/docs/academy/03-ExampleUsecases/Chatroom/02-CreateNewPowerhouseProject.md +111 -35
- package/docs/academy/03-ExampleUsecases/Chatroom/03-DefineChatroomDocumentModel.md +255 -76
- package/docs/academy/03-ExampleUsecases/Chatroom/04-ImplementOperationReducers.md +281 -160
- package/docs/academy/03-ExampleUsecases/Chatroom/05-ImplementChatroomEditor.md +188 -35
- package/docs/academy/03-ExampleUsecases/Chatroom/06-LaunchALocalReactor.md +95 -7
- package/docs/academy/03-ExampleUsecases/Chatroom/_category_.json +1 -1
- package/docs/academy/03-ExampleUsecases/TodoList/00-GetTheStarterCode.md +24 -0
- package/docs/academy/03-ExampleUsecases/TodoList/01-GenerateTodoListDocumentModel.md +211 -0
- package/docs/academy/03-ExampleUsecases/TodoList/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +171 -0
- package/docs/academy/03-ExampleUsecases/TodoList/03-AddTestsForTodoListActions.md +462 -0
- package/docs/academy/03-ExampleUsecases/TodoList/04-GenerateTodoListDocumentEditor.md +45 -0
- package/docs/academy/03-ExampleUsecases/TodoList/05-ImplementTodoListDocumentEditorUIComponents.md +422 -0
- package/docs/academy/03-ExampleUsecases/TodoList/06-GenerateTodoDriveExplorer.md +61 -0
- package/docs/academy/03-ExampleUsecases/TodoList/07-AddSharedComponentForShowingTodoListStats.md +384 -0
- package/docs/academy/03-ExampleUsecases/TodoList/_category_.json +8 -0
- package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/VetraPackageLibrary.md +7 -0
- package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/_category_.json +9 -0
- package/docs/academy/04-APIReferences/00-PowerhouseCLI.md +6 -2
- package/docs/academy/04-APIReferences/01-ReactHooks.md +2 -2
- package/docs/academy/04-APIReferences/06-VetraRemoteDrive.md +160 -0
- package/docs/academy/04-APIReferences/renown-sdk/00-Overview.md +316 -0
- package/docs/academy/04-APIReferences/renown-sdk/01-Authentication.md +672 -0
- package/docs/academy/04-APIReferences/renown-sdk/02-APIReference.md +957 -0
- package/docs/academy/04-APIReferences/renown-sdk/_category_.json +8 -0
- package/docs/academy/05-Architecture/00-PowerhouseArchitecture.md +7 -39
- package/docs/academy/06-ComponentLibrary/00-DocumentEngineering.md +72 -24
- package/docs/academy/08-Glossary.md +7 -0
- package/docs/academy/10-TodoListTutorial/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +171 -0
- package/docs/academy/10-TodoListTutorial/03-AddTestsForTodoListActions.md +462 -0
- package/docs/academy/10-TodoListTutorial/05-ImplementTodoListDocumentEditorUIComponents.md +422 -0
- package/docs/academy/10-TodoListTutorial/07-AddSharedComponentForShowingTodoListStats.md +370 -0
- package/docusaurus.config.ts +28 -3
- package/package.json +1 -1
- package/sidebars.ts +49 -12
- package/src/css/custom.css +26 -18
- package/static/img/Vetra-logo-dark.svg +11 -0
- package/static/img/vetra-logo-light.svg +11 -0
- package/docs/academy/00-EthereumArgentinaHackathon.md +0 -207
- package/docs/academy/01-GetStarted/04-BuildToDoListEditor.md +0 -218
- package/docs/academy/01-GetStarted/06-ReactorMCP.md +0 -58
- package/docs/academy/05-Architecture/02-ReferencingMonorepoPackages +0 -65
- package/docs/academy/05-Architecture/04-MovingBeyondCRUD +0 -61
- /package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/images/Modules.png +0 -0
- /package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/images/VetraStudioDrive.png +0 -0
- /package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/{02-RevisionHistoryTimeline → 02-RevisionHistoryTimeline/360/237/232/247"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{01-WhatIsADocumentModel → 360/237/232/247 /01-WhatIsADocumentModel"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{02-DAOandDocumentsModelsQ+A → 360/237/232/247 /02-DAOandDocumentsModelsQ+A"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{02-domain-modeling → 360/237/232/247 /02-domain-modeling"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{03-BenefitsOfDocumentModels → 360/237/232/247 /03-BenefitsOfDocumentModels"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{04-UtilitiesAndTips → 360/237/232/247 /04-UtilitiesAndTips"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{05-best-practices → 360/237/232/247 /05-best-practices"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{_category_.json → 360/237/232/247 /_category_.json"} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{three-data-layers.png → 360/237/232/247 /three-data-layers.png"} +0 -0
package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/01-BuildingDocumentEditors.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Build document editors
|
|
2
2
|
|
|
3
|
-
## Build with
|
|
3
|
+
## Build with React on Powerhouse
|
|
4
4
|
|
|
5
5
|
At Powerhouse, frontend development for document editors follows a simple and familiar flow, leveraging the power and flexibility of React.
|
|
6
6
|
|
|
@@ -26,13 +26,13 @@ Powerhouse aims to keep your developer experience clean, familiar, and focused:
|
|
|
26
26
|
To kickstart your editor development, Powerhouse provides a command to generate a basic editor template. This command reads your document model specifications and creates the initial `editor.tsx` file.
|
|
27
27
|
If you want a refresher on how to define your document model specification please read the chapter on [specifying the State Schema](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema)
|
|
28
28
|
|
|
29
|
-
For example, to generate an editor for a
|
|
29
|
+
For example, to generate an editor for a TodoList document model with a document type `powerhouse/todo-list`:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
ph generate --editor
|
|
32
|
+
ph generate --editor todo-list-editor --document-types powerhouse/todo-list
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
This will create the template in the `editors/
|
|
35
|
+
This will create the template in the `editors/todo-list-editor/editor.tsx` folder.
|
|
36
36
|
|
|
37
37
|
### Styling your editor
|
|
38
38
|
|
|
@@ -41,11 +41,11 @@ You have several options for styling your editor components:
|
|
|
41
41
|
1. **Default HTML Styling**: Standard HTML tags (`<h1>`, `<p>`, `<button>`, etc.) will render with default browser styles or any base styling provided by the Connect environment. This is suitable for basic structure and quick prototyping.
|
|
42
42
|
|
|
43
43
|
2. **Tailwind CSS**: Connect Studio comes with Tailwind CSS integrated. You can directly use Tailwind utility classes in your JSX for rapid and consistent styling without writing separate CSS files.
|
|
44
|
-
_Example (from the
|
|
44
|
+
_Example (from the TodoList Editor):_
|
|
45
45
|
|
|
46
46
|
```typescript
|
|
47
47
|
<div className="container mx-auto p-4 max-w-md">
|
|
48
|
-
<h1 className="text-2xl font-bold mb-4">
|
|
48
|
+
<h1 className="text-2xl font-bold mb-4">TodoList</h1>
|
|
49
49
|
{/* ... more Tailwind styled elements */}
|
|
50
50
|
</div>
|
|
51
51
|
```
|
|
@@ -103,46 +103,137 @@ In any package the styles are being generated through the styles.css file with t
|
|
|
103
103
|
- When installing a package with `ph install` on any instance, package styles are automatically added to styles.css. This ensures production builds always include the required package styles.
|
|
104
104
|
:::
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
## State management in editors: Hooks vs Props
|
|
107
107
|
|
|
108
|
-
When you build an editor in Powerhouse,
|
|
108
|
+
When you build an editor in Powerhouse, there are **two ways** to access and modify document state. Understanding the difference is important for choosing the right approach for your component.
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
- **`dispatch`**: This function is your gateway to modifying the document's state. You call `dispatch` with an action object (usually created by action creators from your document model's generated code) to signal an intended change.
|
|
110
|
+
### Understanding the two approaches
|
|
112
111
|
|
|
113
|
-
|
|
112
|
+
<details>
|
|
113
|
+
<summary>ℹ️ For Non-Technical Readers</summary>
|
|
114
|
+
|
|
115
|
+
Think of it like ordering food at a restaurant:
|
|
116
|
+
|
|
117
|
+
**Hooks Approach** 🪝: Like having a direct line to the kitchen. Any component can call the kitchen directly to get the current menu (state) or place an order (dispatch an action). It's independent and self-sufficient.
|
|
118
|
+
|
|
119
|
+
**Props Approach** 📦: Like a waiter passing you a menu and taking your order. The main component receives everything and passes it down to child components. Children can only work with what they're given.
|
|
120
|
+
|
|
121
|
+
**Which is better?** For Powerhouse editors, we recommend the **Hooks approach** because:
|
|
122
|
+
- Components are more independent
|
|
123
|
+
- Easier to move components around
|
|
124
|
+
- Less "prop drilling" (passing data through many layers)
|
|
125
|
+
- Matches modern React best practices
|
|
126
|
+
|
|
127
|
+
</details>
|
|
128
|
+
|
|
129
|
+
### Method 1: Using Hooks (Recommended) 🪝
|
|
130
|
+
|
|
131
|
+
The **hook-based approach** uses `useSelectedTodoListDocument` — a React hook that Powerhouse generates for your document model. Any component can call this hook to get the current document state and a function to dispatch changes.
|
|
114
132
|
|
|
115
|
-
- **Local Component State**: For UI-specific state that doesn't need to be part of the persisted document model (e.g., the current text in an input field before submission, visibility of a dropdown), use React's `useState` hook.
|
|
116
133
|
```typescript
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
import { useSelectedTodoListDocument } from "todo-tutorial/document-models/todo-list";
|
|
135
|
+
import { addTodoItem } from "todo-tutorial/document-models/todo-list";
|
|
136
|
+
|
|
137
|
+
export function AddTodo() {
|
|
138
|
+
// The hook returns [document, dispatch]
|
|
139
|
+
const [todoList, dispatch] = useSelectedTodoListDocument();
|
|
140
|
+
|
|
141
|
+
if (!todoList) return null;
|
|
122
142
|
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
const handleAdd = (text: string) => {
|
|
144
|
+
dispatch(addTodoItem({ text }));
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<button onClick={() => handleAdd("New task")}>
|
|
149
|
+
Add Todo
|
|
150
|
+
</button>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Why hooks are recommended:**
|
|
156
|
+
- ✅ **Self-contained components**: Each component gets its own connection to the document
|
|
157
|
+
- ✅ **Less boilerplate**: No need to pass props through multiple levels
|
|
158
|
+
- ✅ **Easier refactoring**: Move components around without rewiring props
|
|
159
|
+
- ✅ **Modern React pattern**: Follows React's recommended approach for state management
|
|
160
|
+
|
|
161
|
+
### Method 2: Using Props 📦
|
|
162
|
+
|
|
163
|
+
The **props-based approach** receives the document and dispatch function as properties passed from a parent component.
|
|
125
164
|
|
|
126
165
|
```typescript
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
import { EditorProps } from 'document-model';
|
|
167
|
+
import { TodoListDocument } from '../../document-models/todo-list/index.js';
|
|
168
|
+
|
|
169
|
+
export type IProps = EditorProps<TodoListDocument>;
|
|
170
|
+
|
|
171
|
+
export default function Editor(props: IProps) {
|
|
172
|
+
const { document, dispatch } = props;
|
|
173
|
+
const state = document.state.global;
|
|
174
|
+
|
|
175
|
+
// Now you'd pass state and dispatch to child components as props
|
|
176
|
+
return (
|
|
177
|
+
<div>
|
|
178
|
+
<TodoList items={state.items} dispatch={dispatch} />
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
143
182
|
```
|
|
144
183
|
|
|
145
|
-
|
|
184
|
+
**When props might be useful:**
|
|
185
|
+
- When you need strict control over which components can access state
|
|
186
|
+
- When building components that should work outside of Powerhouse context
|
|
187
|
+
- For testing purposes where you want to inject mock state
|
|
188
|
+
|
|
189
|
+
### Which should you use?
|
|
190
|
+
|
|
191
|
+
| Scenario | Recommended Approach |
|
|
192
|
+
|----------|---------------------|
|
|
193
|
+
| Building a standard Powerhouse editor | **Hooks** 🪝 |
|
|
194
|
+
| Component needs document state | **Hooks** 🪝 |
|
|
195
|
+
| Building reusable UI components (buttons, inputs) | **Props** 📦 |
|
|
196
|
+
| Need to test components in isolation | **Props** 📦 |
|
|
197
|
+
|
|
198
|
+
**Bottom line**: Use hooks for most Powerhouse editor development. It's simpler, cleaner, and matches the patterns used in the [todo-demo repository](https://github.com/powerhouse-inc/todo-demo).
|
|
199
|
+
|
|
200
|
+
## Local vs. Global State
|
|
201
|
+
|
|
202
|
+
When building editors, you'll work with two types of state:
|
|
203
|
+
|
|
204
|
+
- **Global Document State**: Data that is part of the document itself and should be saved. This is accessed via hooks (`useSelectedTodoListDocument`) or props (`document.state.global`). You modify it by dispatching actions.
|
|
205
|
+
|
|
206
|
+
- **Local Component State**: UI-specific state that doesn't need to be saved (e.g., "is the dropdown open?", "what's in the input field before submission?"). Use React's `useState` hook for this.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { useState } from 'react';
|
|
210
|
+
import { useSelectedTodoListDocument, addTodoItem } from "todo-tutorial/document-models/todo-list";
|
|
211
|
+
|
|
212
|
+
export function AddTodo() {
|
|
213
|
+
// Local state - just for this component's UI
|
|
214
|
+
const [inputValue, setInputValue] = useState('');
|
|
215
|
+
|
|
216
|
+
// Global document state - saved in the document
|
|
217
|
+
const [todoList, dispatch] = useSelectedTodoListDocument();
|
|
218
|
+
|
|
219
|
+
const handleSubmit = () => {
|
|
220
|
+
if (inputValue.trim()) {
|
|
221
|
+
dispatch(addTodoItem({ text: inputValue })); // Updates global state
|
|
222
|
+
setInputValue(''); // Clears local state
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div>
|
|
228
|
+
<input
|
|
229
|
+
value={inputValue}
|
|
230
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
231
|
+
/>
|
|
232
|
+
<button onClick={handleSubmit}>Add</button>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
146
237
|
|
|
147
238
|
## Powerhouse component library
|
|
148
239
|
|
|
@@ -184,24 +275,24 @@ Storybook allows you to:
|
|
|
184
275
|
```
|
|
185
276
|
|
|
186
277
|
<details>
|
|
187
|
-
<summary>Tutorial: Implementing the
|
|
278
|
+
<summary>Tutorial: Implementing the TodoList Editor</summary>
|
|
188
279
|
|
|
189
|
-
## Build a
|
|
280
|
+
## Build a TodoList editor
|
|
190
281
|
|
|
191
|
-
In this final part of our tutorial we will continue with the interface or editor implementation of the **
|
|
282
|
+
In this final part of our tutorial we will continue with the interface or editor implementation of the **TodoList** document model. This means you will create a simple user interface for the **TodoList** document model which will be used inside the Connect app to create, update and delete your TodoList items, and also display the statistics we've implemented in our reducers (if you followed the advanced version).
|
|
192
283
|
|
|
193
284
|
## Generate the editor template
|
|
194
285
|
|
|
195
|
-
Run the command below to generate the editor template for the **
|
|
196
|
-
This command reads the **
|
|
286
|
+
Run the command below to generate the editor template for the **TodoList** document model.
|
|
287
|
+
This command reads the **TodoList** document model definition from the `document-models` folder and generates the editor template in the `editors/todo-list-editor` folder as `editor.tsx`.
|
|
197
288
|
|
|
198
|
-
Notice the `--editor` flag which specifies the
|
|
289
|
+
Notice the `--editor` flag which specifies the editor name, and the `--document-types` flag defines the document type `powerhouse/todo-list`.
|
|
199
290
|
|
|
200
291
|
```bash
|
|
201
|
-
ph generate --editor
|
|
292
|
+
ph generate --editor todo-list-editor --document-types powerhouse/todo-list
|
|
202
293
|
```
|
|
203
294
|
|
|
204
|
-
Once complete, navigate to the `editors/
|
|
295
|
+
Once complete, navigate to the `editors/todo-list-editor/editor.tsx` file and open it in your editor.
|
|
205
296
|
|
|
206
297
|
### Editor implementation options
|
|
207
298
|
|
|
@@ -215,323 +306,222 @@ Connect Studio provides a dynamic local environment (`ph connect`) to visualize
|
|
|
215
306
|
|
|
216
307
|
---
|
|
217
308
|
|
|
218
|
-
##
|
|
219
|
-
|
|
220
|
-
:::tip Implementing components
|
|
221
|
-
The editor we are about to implement makes use of some components from **Powerhouse Document Engineering**.
|
|
222
|
-
When you add the editor code, you'll see it makes use of two components, the `Checkbox` and `InputField`.
|
|
223
|
-
These are imported from the Powerhouse Document Engineering design system (`@powerhousedao/document-engineering/scalars`) which you should find under 'devdependencies' in your package.json file.
|
|
309
|
+
## TodoList editor using hooks (Recommended)
|
|
224
310
|
|
|
225
|
-
This
|
|
226
|
-
You can explore available components, see usage examples, and understand their properties (props) using our Storybook instance. For a detailed guide on how to leverage the Document Engineering design system and Storybook, see [Using the Powerhouse Document Engineering](/academy/ComponentLibrary/DocumentEngineering) page.
|
|
311
|
+
This approach uses the `useSelectedTodoListDocument` hook, which is the same pattern used in the Get Started tutorial and the [todo-demo repository](https://github.com/powerhouse-inc/todo-demo).
|
|
227
312
|
|
|
228
|
-
|
|
229
|
-
:::
|
|
313
|
+
### Main editor file
|
|
230
314
|
|
|
231
|
-
<details>
|
|
232
|
-
<summary>Checkbox</summary>
|
|
233
315
|
```typescript
|
|
234
|
-
|
|
316
|
+
// editors/todo-list-editor/editor.tsx
|
|
317
|
+
import { TodoList } from "./components/TodoList.js";
|
|
235
318
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
319
|
+
export function Editor() {
|
|
320
|
+
return (
|
|
321
|
+
<div className="py-4 px-8">
|
|
322
|
+
<TodoList />
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
239
325
|
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### TodoList container component
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// editors/todo-list-editor/components/TodoList.tsx
|
|
332
|
+
import { useSelectedTodoListDocument } from "todo-tutorial/document-models/todo-list";
|
|
333
|
+
import { Todos } from "./Todos.js";
|
|
334
|
+
import { AddTodo } from "./AddTodo.js";
|
|
335
|
+
|
|
336
|
+
export function TodoList() {
|
|
337
|
+
const [selectedTodoList] = useSelectedTodoListDocument();
|
|
338
|
+
|
|
339
|
+
if (!selectedTodoList) return null;
|
|
340
|
+
|
|
341
|
+
const todos = selectedTodoList.state.global.items;
|
|
240
342
|
|
|
241
|
-
export const Checkbox = ({ value, onChange }: CheckboxProps) => {
|
|
242
343
|
return (
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
</
|
|
251
|
-
|
|
252
|
-
|
|
344
|
+
<div>
|
|
345
|
+
<h1 className="text-2xl font-bold mb-4">TodoList</h1>
|
|
346
|
+
<section className="mb-4">
|
|
347
|
+
<Todos todos={todos} />
|
|
348
|
+
</section>
|
|
349
|
+
<section>
|
|
350
|
+
<AddTodo />
|
|
351
|
+
</section>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
```
|
|
253
356
|
|
|
254
|
-
|
|
255
|
-
</details>
|
|
357
|
+
### AddTodo component
|
|
256
358
|
|
|
257
|
-
<details>
|
|
258
|
-
<summary>Inputfield</summary>
|
|
259
359
|
```typescript
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
267
|
-
handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
268
|
-
}
|
|
360
|
+
// editors/todo-list-editor/components/AddTodo.tsx
|
|
361
|
+
import type { FormEventHandler } from "react";
|
|
362
|
+
import { addTodoItem, useSelectedTodoListDocument } from "todo-tutorial/document-models/todo-list";
|
|
363
|
+
|
|
364
|
+
export function AddTodo() {
|
|
365
|
+
const [todoList, dispatch] = useSelectedTodoListDocument();
|
|
269
366
|
|
|
270
|
-
|
|
271
|
-
|
|
367
|
+
if (!todoList) return null;
|
|
368
|
+
|
|
369
|
+
const onSubmitAddTodo: FormEventHandler<HTMLFormElement> = (event) => {
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
|
|
372
|
+
const form = event.currentTarget;
|
|
373
|
+
const addTodoInput = form.elements.namedItem("addTodo") as HTMLInputElement;
|
|
374
|
+
const text = addTodoInput.value;
|
|
375
|
+
if (!text) return;
|
|
376
|
+
|
|
377
|
+
dispatch(addTodoItem({ text }));
|
|
378
|
+
form.reset();
|
|
379
|
+
};
|
|
272
380
|
|
|
273
381
|
return (
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
<StringField
|
|
282
|
-
style={{
|
|
283
|
-
color: "black",
|
|
284
|
-
}}
|
|
285
|
-
label={label}
|
|
286
|
-
name="input"
|
|
287
|
-
value={value}
|
|
288
|
-
onKeyDown={onKeyDown}
|
|
289
|
-
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
290
|
-
handleInputChange(e);
|
|
291
|
-
}}
|
|
382
|
+
<form onSubmit={onSubmitAddTodo} className="flex mx-auto min-w-fit gap-2">
|
|
383
|
+
<input
|
|
384
|
+
className="py-1 px-2 grow min-w-fit placeholder:text-gray-600 rounded border border-gray-600 text-gray-800"
|
|
385
|
+
type="text"
|
|
386
|
+
name="addTodo"
|
|
387
|
+
placeholder="What needs to be done?"
|
|
388
|
+
autoFocus
|
|
292
389
|
/>
|
|
293
|
-
|
|
390
|
+
<button
|
|
391
|
+
type="submit"
|
|
392
|
+
className="text-gray-600 rounded border border-gray-600 px-3 py-1"
|
|
393
|
+
>
|
|
394
|
+
Add
|
|
395
|
+
</button>
|
|
396
|
+
</form>
|
|
294
397
|
);
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Todo item component
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// editors/todo-list-editor/components/Todo.tsx
|
|
405
|
+
import { useState, type ChangeEventHandler, type FormEventHandler, type MouseEventHandler } from "react";
|
|
406
|
+
import { deleteTodoItem, updateTodoItem, useSelectedTodoListDocument } from "todo-tutorial/document-models/todo-list";
|
|
407
|
+
import type { TodoItem } from "todo-tutorial/document-models/todo-list";
|
|
408
|
+
|
|
409
|
+
type Props = {
|
|
410
|
+
todo: TodoItem;
|
|
295
411
|
};
|
|
296
|
-
````
|
|
297
412
|
|
|
298
|
-
|
|
413
|
+
export function Todo({ todo }: Props) {
|
|
414
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
415
|
+
const [todoList, dispatch] = useSelectedTodoListDocument();
|
|
299
416
|
|
|
300
|
-
|
|
417
|
+
if (!todoList) return null;
|
|
301
418
|
|
|
302
|
-
<
|
|
303
|
-
|
|
419
|
+
const onChangeTodoChecked: ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
420
|
+
dispatch(updateTodoItem({ id: todo.id, checked: event.target.checked }));
|
|
421
|
+
};
|
|
304
422
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
export type IProps = EditorProps<ToDoListDocument>;
|
|
321
|
-
|
|
322
|
-
// Define the main Editor component function.
|
|
323
|
-
export default function Editor(props: IProps) {
|
|
324
|
-
// Destructure props for easier access.
|
|
325
|
-
const { document, dispatch } = props;
|
|
326
|
-
// Access the global state from the document object.
|
|
327
|
-
const { state: { global: state } } = document;
|
|
328
|
-
|
|
329
|
-
// --- Component State ---
|
|
330
|
-
// State for the text input field where new tasks are typed.
|
|
331
|
-
const [todoItem, setTodoItem] = useState('');
|
|
332
|
-
// State to track which item is currently being edited (null if none). Stores the item's ID.
|
|
333
|
-
const [editingItemId, setEditingItemId] = useState<string | null>(null);
|
|
334
|
-
// State to hold the text of the item currently being edited.
|
|
335
|
-
const [editedText, setEditedText] = useState('');
|
|
336
|
-
|
|
337
|
-
// Sort items to show unchecked items first
|
|
338
|
-
const sortedItems: ToDoItem[] = [...state.items].sort((a, b) => {
|
|
339
|
-
return (a.checked ? 1 : 0) - (b.checked ? 1 : 0);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// --- JSX Structure (What gets rendered) ---
|
|
423
|
+
const onClickDeleteTodo: MouseEventHandler<HTMLButtonElement> = () => {
|
|
424
|
+
dispatch(deleteTodoItem({ id: todo.id }));
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const onSubmitUpdateTodoText: FormEventHandler<HTMLFormElement> = (event) => {
|
|
428
|
+
event.preventDefault();
|
|
429
|
+
const form = event.currentTarget;
|
|
430
|
+
const textInput = form.elements.namedItem("todoText") as HTMLInputElement;
|
|
431
|
+
const text = textInput.value;
|
|
432
|
+
if (!text) return;
|
|
433
|
+
dispatch(updateTodoItem({ id: todo.id, text }));
|
|
434
|
+
setIsEditing(false);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
if (isEditing) {
|
|
343
438
|
return (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
439
|
+
<form className="flex gap-2 items-center" onSubmit={onSubmitUpdateTodoText}>
|
|
440
|
+
<input className="p-1 grow" type="text" name="todoText" defaultValue={todo.text} autoFocus />
|
|
441
|
+
<button type="submit" className="text-sm text-gray-600">Save</button>
|
|
442
|
+
<button className="text-sm text-red-800" onClick={() => setIsEditing(false)}>Cancel</button>
|
|
443
|
+
</form>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<div className="flex justify-between items-center">
|
|
449
|
+
<div className="flex items-center gap-2 p-1">
|
|
450
|
+
<input type="checkbox" checked={todo.checked} onChange={onChangeTodoChecked} />
|
|
451
|
+
<span className={todo.checked ? "line-through" : ""}>{todo.text}</span>
|
|
452
|
+
</div>
|
|
453
|
+
<span className="flex place-items-center gap-2 text-sm">
|
|
454
|
+
<button className="text-gray-600" onClick={() => setIsEditing(true)}>Edit</button>
|
|
455
|
+
<button className="text-red-800" onClick={onClickDeleteTodo}>Delete</button>
|
|
456
|
+
</span>
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Advanced: Adding stats display
|
|
465
|
+
|
|
466
|
+
:::info Advanced Feature
|
|
467
|
+
If you implemented the advanced version with statistics tracking, you can add a stats component to display the todo counts.
|
|
468
|
+
:::
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// Add to TodoList.tsx
|
|
472
|
+
export function TodoList() {
|
|
473
|
+
const [selectedTodoList] = useSelectedTodoListDocument();
|
|
474
|
+
|
|
475
|
+
if (!selectedTodoList) return null;
|
|
476
|
+
|
|
477
|
+
const { items, stats } = selectedTodoList.state.global;
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<div>
|
|
481
|
+
<h1 className="text-2xl font-bold mb-4">TodoList</h1>
|
|
482
|
+
|
|
483
|
+
{/* Stats section (only show if there are items) */}
|
|
484
|
+
{items.length >= 2 && (
|
|
358
485
|
<div className="mb-4 bg-white rounded-lg px-3 py-2 shadow-md">
|
|
359
486
|
<div className="grid grid-cols-3 gap-3">
|
|
360
487
|
<div>
|
|
361
|
-
|
|
362
|
-
|
|
488
|
+
<div className="text-xs text-slate-500">Total</div>
|
|
489
|
+
<div className="text-lg font-semibold">{stats.total}</div>
|
|
363
490
|
</div>
|
|
364
491
|
<div>
|
|
365
|
-
|
|
366
|
-
|
|
492
|
+
<div className="text-xs text-slate-500">Completed</div>
|
|
493
|
+
<div className="text-lg font-semibold text-green-600">{stats.checked}</div>
|
|
367
494
|
</div>
|
|
368
495
|
<div>
|
|
369
|
-
|
|
370
|
-
|
|
496
|
+
<div className="text-xs text-slate-500">Remaining</div>
|
|
497
|
+
<div className="text-lg font-semibold text-orange-600">{stats.unchecked}</div>
|
|
371
498
|
</div>
|
|
372
499
|
</div>
|
|
373
500
|
</div>
|
|
374
501
|
)}
|
|
375
502
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
<div className="flex-grow">
|
|
383
|
-
<InputField
|
|
384
|
-
label="New Task" // Prop for accessibility/placeholder.
|
|
385
|
-
input={todoItem} // Current value from state.
|
|
386
|
-
value={todoItem} // Controlled component value.
|
|
387
|
-
handleInputChange={(e) => setTodoItem(e.target.value)} // Update state on change.
|
|
388
|
-
onKeyDown={(e) => { // Handle "Enter" key press to add item.
|
|
389
|
-
if (e.key === 'Enter' && todoItem.trim()) { // Check if key is Enter and input is not empty
|
|
390
|
-
dispatch(actions.addTodoItem({ // Dispatch action to add item.
|
|
391
|
-
id: Math.random().toString(), // Generate a simple unique ID (use a better method in production!).
|
|
392
|
-
text: todoItem,
|
|
393
|
-
}));
|
|
394
|
-
setTodoItem(''); // Clear the input field.
|
|
395
|
-
}
|
|
396
|
-
}}
|
|
397
|
-
/>
|
|
398
|
-
</div>
|
|
399
|
-
{/* "Add" button */}
|
|
400
|
-
{/* `bg-blue-500`: Sets background color to blue. */}
|
|
401
|
-
{/* `hover:bg-blue-600`: Changes background color on hover. */}
|
|
402
|
-
{/* `text-white`: Sets text color to white. */}
|
|
403
|
-
{/* `px-4`: Adds horizontal padding (4 units). */}
|
|
404
|
-
{/* `py-1.5`: Adds vertical padding (1.5 units). */}
|
|
405
|
-
{/* `rounded`: Applies rounded corners. */}
|
|
406
|
-
{/* `transition-colors`: Smoothly animates color changes. */}
|
|
407
|
-
<button
|
|
408
|
-
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-1.5 rounded transition-colors"
|
|
409
|
-
onClick={() => { // Handle button click to add item.
|
|
410
|
-
if (todoItem.trim()) { // Check if input is not empty
|
|
411
|
-
dispatch(actions.addTodoItem({ // Dispatch action to add item.
|
|
412
|
-
id: Math.random().toString(), // Simple unique ID.
|
|
413
|
-
text: todoItem,
|
|
414
|
-
}));
|
|
415
|
-
setTodoItem(''); // Clear the input field.
|
|
416
|
-
}
|
|
417
|
-
}}
|
|
418
|
-
>
|
|
419
|
-
Add
|
|
420
|
-
</button>
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
{/* Unordered list to display the to-do items */}
|
|
424
|
-
{/* `list-none`: Removes default list bullet points. */}
|
|
425
|
-
{/* `p-0`: Removes default padding. */}
|
|
426
|
-
<ul className="list-none p-0">
|
|
427
|
-
{/* Map over the items array in the global state to render each item */}
|
|
428
|
-
{sortedItems.map((item: ToDoItem) => (
|
|
429
|
-
// List item element for each to-do.
|
|
430
|
-
// `key={item.id}`: React requires a unique key for list items for efficient updates.
|
|
431
|
-
// `flex`: Enables flexbox layout (checkbox, text, delete icon in a row).
|
|
432
|
-
// `items-center`: Aligns items vertically in the center.
|
|
433
|
-
// `p-2`: Adds padding.
|
|
434
|
-
// `relative`: Needed for positioning the delete icon absolutely (if we were doing that).
|
|
435
|
-
// `border-b`: Adds a bottom border.
|
|
436
|
-
// `border-gray-100`: Sets border color to light gray.
|
|
437
|
-
<li
|
|
438
|
-
key={item.id}
|
|
439
|
-
className="flex items-center p-2 relative border-b border-gray-100"
|
|
440
|
-
>
|
|
441
|
-
{/* Custom Checkbox component */}
|
|
442
|
-
<Checkbox
|
|
443
|
-
value={item.checked} // Bind checked state to item's checked property.
|
|
444
|
-
onChange={() => { // Handle checkbox click.
|
|
445
|
-
dispatch(actions.updateTodoItem({ // Dispatch action to update item.
|
|
446
|
-
id: item.id,
|
|
447
|
-
checked: !item.checked, // Toggle the checked state.
|
|
448
|
-
}));
|
|
449
|
-
}}
|
|
450
|
-
/>
|
|
451
|
-
|
|
452
|
-
{/* Conditional Rendering: Show input field or text based on editing state */}
|
|
453
|
-
{editingItemId === item.id ? (
|
|
454
|
-
// --- Editing State ---
|
|
455
|
-
// Input field shown when this item is being edited.
|
|
456
|
-
// `ml-2`: Adds left margin.
|
|
457
|
-
// `flex-grow`: Allows input to take available horizontal space.
|
|
458
|
-
// `p-1`: Adds small padding.
|
|
459
|
-
// `border`: Adds a default border.
|
|
460
|
-
// `rounded`: Applies rounded corners.
|
|
461
|
-
// `focus:outline-none`: Removes the default browser focus outline.
|
|
462
|
-
// `focus:ring-1 focus:ring-blue-500`: Adds a custom blue ring when focused.
|
|
463
|
-
<input
|
|
464
|
-
className="ml-2 flex-grow p-1 border rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
465
|
-
value={editedText} // Controlled input value from editedText state.
|
|
466
|
-
onChange={(e) => setEditedText(e.target.value)} // Update editedText state.
|
|
467
|
-
onKeyDown={(e) => { // Handle "Enter" key to save changes.
|
|
468
|
-
if (e.key === 'Enter') {
|
|
469
|
-
dispatch(actions.updateTodoItem({ // Dispatch update action.
|
|
470
|
-
id: item.id,
|
|
471
|
-
text: editedText, // Save the edited text.
|
|
472
|
-
}));
|
|
473
|
-
setEditingItemId(null); // Exit editing mode.
|
|
474
|
-
}
|
|
475
|
-
}}
|
|
476
|
-
autoFocus // Automatically focus the input when it appears.
|
|
477
|
-
/>
|
|
478
|
-
) : (
|
|
479
|
-
// --- Display State ---
|
|
480
|
-
// Container for the item text and delete icon when not editing.
|
|
481
|
-
// `ml-2`: Adds left margin.
|
|
482
|
-
// `flex items-center`: Aligns text and icon vertically.
|
|
483
|
-
// `flex-grow`: Allows this container to take available space.
|
|
484
|
-
// `gap-1`: Adds a small gap between text and icon.
|
|
485
|
-
<div className="ml-2 flex items-center flex-grow gap-1">
|
|
486
|
-
{/* The actual to-do item text */}
|
|
487
|
-
{/* `cursor-pointer`: Shows a pointer cursor on hover, indicating clickability. */}
|
|
488
|
-
{/* Conditional class: Apply line-through and gray text if item is checked. */}
|
|
489
|
-
{/* `line-through`: Strikes through the text. */}
|
|
490
|
-
{/* `text-gray-500`: Sets text color to gray. */}
|
|
491
|
-
<span
|
|
492
|
-
className={`cursor-pointer ${item.checked ? 'line-through text-gray-500' : ''}`}
|
|
493
|
-
onClick={() => { // Handle click to enter editing mode.
|
|
494
|
-
setEditingItemId(item.id); // Set the ID of the item being edited.
|
|
495
|
-
setEditedText(item.text); // Initialize the input with current text.
|
|
496
|
-
}}
|
|
497
|
-
>
|
|
498
|
-
{item.text} {/* Display the item's text */}
|
|
499
|
-
</span>
|
|
500
|
-
{/* Delete "button" (using a span styled as a button) */}
|
|
501
|
-
{/* `text-gray-400`: Sets default text color to light gray. */}
|
|
502
|
-
{/* `cursor-pointer`: Shows pointer cursor. */}
|
|
503
|
-
{/* `opacity-40`: Makes it semi-transparent by default. */}
|
|
504
|
-
{/* `transition-all duration-200`: Smoothly animates all changes (opacity, color). */}
|
|
505
|
-
{/* `text-base font-bold`: Sets text size and weight. */}
|
|
506
|
-
{/* `inline-flex items-center`: Needed for proper alignment if using an icon font/SVG. */}
|
|
507
|
-
{/* `pl-1`: Adds small left padding. */}
|
|
508
|
-
{/* `hover:opacity-100`: Makes it fully opaque on hover. */}
|
|
509
|
-
{/* `hover:text-red-500`: Changes text color to red on hover. */}
|
|
510
|
-
<span
|
|
511
|
-
className="text-gray-400 cursor-pointer opacity-40 transition-all duration-200 text-base font-bold inline-flex items-center pl-1 hover:opacity-100 hover:text-red-500"
|
|
512
|
-
onClick={() => dispatch(actions.deleteTodoItem({ id: item.id }))} // Dispatch delete action on click.
|
|
513
|
-
>
|
|
514
|
-
× {/* Simple multiplication sign used as delete icon */}
|
|
515
|
-
</span>
|
|
516
|
-
</div>
|
|
517
|
-
)}
|
|
518
|
-
</li>
|
|
519
|
-
))}
|
|
520
|
-
</ul>
|
|
503
|
+
<section className="mb-4">
|
|
504
|
+
<Todos todos={items} />
|
|
505
|
+
</section>
|
|
506
|
+
<section>
|
|
507
|
+
<AddTodo />
|
|
508
|
+
</section>
|
|
521
509
|
</div>
|
|
522
510
|
);
|
|
523
511
|
}
|
|
524
512
|
```
|
|
525
513
|
|
|
526
|
-
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Test your editor
|
|
527
517
|
|
|
528
|
-
Now you can run the Connect app and see the **
|
|
518
|
+
Now you can run the Connect app and see the **TodoList** editor in action:
|
|
529
519
|
|
|
530
520
|
```bash
|
|
531
521
|
ph connect
|
|
532
522
|
```
|
|
533
523
|
|
|
534
|
-
In Connect, in the bottom right corner you'll find a new Document Model that you can create: **
|
|
524
|
+
In Connect, in the bottom right corner you'll find a new Document Model that you can create: **TodoList**. Click on it to create a new TodoList document.
|
|
535
525
|
|
|
536
526
|
:::tip Connect as your dynamic development environment
|
|
537
527
|
The editor will update dynamically, so you can play around with your editor styling while seeing your results appear in Connect Studio.
|
|
@@ -540,9 +530,9 @@ The editor will update dynamically, so you can play around with your editor styl
|
|
|
540
530
|
</details>
|
|
541
531
|
|
|
542
532
|
Congratulations!
|
|
543
|
-
If you managed to follow this tutorial until this point, you have successfully implemented the **
|
|
533
|
+
If you managed to follow this tutorial until this point, you have successfully implemented the **TodoList** document model with its reducer operations and editor.
|
|
544
534
|
|
|
545
535
|
## Up Next
|
|
546
536
|
|
|
547
|
-
Now you can move on to creating a [custom drive explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) for your
|
|
548
|
-
Imagine you have many
|
|
537
|
+
Now you can move on to creating a [custom drive explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) for your TodoList document.
|
|
538
|
+
Imagine you have many TodoLists sitting in a drive. A custom drive explorer will allow you to organize and track them at a glance, opening up a new world of possibilities to increase the functionality of your documents!
|