Skip to content

Commit 51da13f

Browse files
Merge pull request #1496 from LeighFinegold/calm-widgets
Adding DotNotation Support and Widget Framework to existing cli (#1495)
2 parents c91c98d + 748d4e5 commit 51da13f

File tree

90 files changed

+7024
-109
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+7024
-109
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Build Calm Widgets
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
pull_request:
8+
branches:
9+
- 'main'
10+
push:
11+
branches:
12+
- 'main'
13+
14+
jobs:
15+
shared:
16+
name: Build, Test, and Lint Calm Widgets Module
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Checkout PR Branch
21+
uses: actions/checkout@v4
22+
23+
- name: Setup Node.js
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: v22
27+
28+
- name: Install workspace
29+
run: npm ci
30+
31+
- name: Lint Shared Module
32+
run: npm run lint --workspace=calm-widgets
33+
34+
- name: Build workspace
35+
run: npm run build --workspace=calm-widgets
36+
37+
- name: Run tests with coverage for Calm Widgets
38+
run: npm run test --workspace=calm-widgets

.github/workflows/build-shared.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
run: npm run lint --workspace=shared
3333

3434
- name: Build workspace
35-
run: npm run build --workspace=shared
35+
run: npm run build:shared
3636

3737
- name: Run tests with coverage for Shared
3838
run: npm run test --workspace=shared

calm-widgets/README.md

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
# CALM Widgets Framework
2+
3+
A TypeScript widget system built on Handlebars that provides reusable components for generating Markdown documentation. The framework allows you to create custom widgets that can transform data into formatted output using Handlebars templates.
4+
5+
## 🔧 Built-in Widgets
6+
7+
### Table Widget
8+
9+
Renders data as Markdown tables with support for nested objects and column filtering.
10+
11+
```handlebars
12+
{{!-- Basic table with headers --}}
13+
{{table services}}
14+
15+
{{!-- Table without headers --}}
16+
{{table services headers=false}}
17+
18+
{{!-- Filter specific columns --}}
19+
{{table services columns="name,port,version" key="id"}}
20+
```
21+
22+
**Options:**
23+
- `headers` (boolean): Show/hide table headers (default: true)
24+
- `columns` (string): Comma-separated list of columns to include
25+
- `key` (string): Property to use as unique identifier (default: "unique-id")
26+
27+
### List Widget
28+
29+
Renders arrays as Markdown lists (ordered or unordered).
30+
31+
```handlebars
32+
{{!-- Unordered list --}}
33+
{{list features}}
34+
35+
{{!-- Ordered list --}}
36+
{{list steps ordered=true}}
37+
38+
{{!-- Extract specific property from objects --}}
39+
{{list services property="name"}}
40+
```
41+
42+
**Options:**
43+
- `ordered` (boolean): Create numbered list (default: false)
44+
- `property` (string): Extract specific property from objects
45+
46+
### JSON Viewer Widget
47+
48+
Renders data as formatted JSON blocks.
49+
50+
```handlebars
51+
{{!-- Simple JSON output --}}
52+
{{json-viewer config}}
53+
```
54+
55+
## 🛠️ Creating Custom Widgets
56+
57+
### 1. Widget Definition
58+
59+
Create a widget by implementing the `CalmWidget` interface:
60+
61+
```typescript
62+
// src/widgets/my-widget/index.ts
63+
import { CalmWidget } from '@finos/calm-widgets';
64+
65+
export interface MyWidgetContext {
66+
title: string;
67+
items: string[];
68+
}
69+
70+
export interface MyWidgetOptions {
71+
showCount?: boolean;
72+
prefix?: string;
73+
}
74+
75+
export interface MyWidgetViewModel {
76+
title: string;
77+
items: string[];
78+
count?: number;
79+
prefix: string;
80+
}
81+
82+
export const MyWidget: CalmWidget<
83+
MyWidgetContext,
84+
MyWidgetOptions,
85+
MyWidgetViewModel
86+
> = {
87+
id: 'my-widget',
88+
templatePartial: 'my-widget-template.html',
89+
90+
// Optional: additional template partials
91+
partials: ['item-template.html'],
92+
93+
// Transform input data to view model
94+
transformToViewModel: (context, options) => {
95+
const showCount = options?.hash?.showCount ?? false;
96+
const prefix = options?.hash?.prefix ?? '';
97+
98+
return {
99+
title: context.title,
100+
items: context.items,
101+
count: showCount ? context.items.length : undefined,
102+
prefix
103+
};
104+
},
105+
106+
// Validate input context
107+
validateContext: (context): context is MyWidgetContext => {
108+
return (
109+
typeof context === 'object' &&
110+
context !== null &&
111+
typeof (context as any).title === 'string' &&
112+
Array.isArray((context as any).items) &&
113+
(context as any).items.every((item: any) => typeof item === 'string')
114+
);
115+
},
116+
117+
// Optional: register custom helpers
118+
registerHelpers: () => ({
119+
upperCase: (str: string) => str.toUpperCase(),
120+
repeat: (str: string, count: number) => str.repeat(count)
121+
})
122+
};
123+
```
124+
125+
### 2. Template Files
126+
127+
Create Handlebars templates for your widget:
128+
129+
```handlebars
130+
<!-- src/widgets/my-widget/my-widget-template.html -->
131+
## {{title}}
132+
{{#if count}}
133+
*Total items: {{count}}*
134+
{{/if}}
135+
136+
{{#each items}}
137+
{{../prefix}} {{upperCase this}}
138+
{{/each}}
139+
```
140+
141+
```handlebars
142+
<!-- src/widgets/my-widget/item-template.html -->
143+
{{prefix}} **{{upperCase this}}**
144+
```
145+
146+
### 3. Widget Tests
147+
148+
Create comprehensive tests for your widget:
149+
150+
```typescript
151+
// src/widgets/my-widget/index.spec.ts
152+
import { describe, it, expect } from 'vitest';
153+
import { MyWidget } from './index';
154+
155+
describe('MyWidget', () => {
156+
describe('validateContext', () => {
157+
it('accepts valid context', () => {
158+
const context = {
159+
title: 'Test Title',
160+
items: ['item1', 'item2']
161+
};
162+
expect(MyWidget.validateContext(context)).toBe(true);
163+
});
164+
165+
it('rejects invalid context', () => {
166+
expect(MyWidget.validateContext(null)).toBe(false);
167+
expect(MyWidget.validateContext({ title: 123 })).toBe(false);
168+
});
169+
});
170+
171+
describe('transformToViewModel', () => {
172+
it('transforms context correctly', () => {
173+
const context = { title: 'Test', items: ['a', 'b'] };
174+
const options = { hash: { showCount: true, prefix: '-' } };
175+
176+
const result = MyWidget.transformToViewModel!(context, options);
177+
178+
expect(result).toEqual({
179+
title: 'Test',
180+
items: ['a', 'b'],
181+
count: 2,
182+
prefix: '-'
183+
});
184+
});
185+
});
186+
});
187+
```
188+
189+
### 4. Test Fixtures
190+
191+
Create test fixtures to verify widget output:
192+
193+
```json
194+
// test-fixtures/my-widget/basic-example/context.json
195+
{
196+
"title": "My Items",
197+
"items": ["First Item", "Second Item", "Third Item"]
198+
}
199+
```
200+
201+
```handlebars
202+
{{!-- test-fixtures/my-widget/basic-example/template.hbs --}}
203+
{{my-widget . showCount=true prefix="→"}}
204+
```
205+
206+
```markdown
207+
<!-- test-fixtures/my-widget/basic-example/expected.md -->
208+
## My Items
209+
*Total items: 3*
210+
211+
→ FIRST ITEM
212+
→ SECOND ITEM
213+
→ THIRD ITEM
214+
```
215+
216+
### 5. Register Your Widget
217+
218+
Add your widget to the engine:
219+
220+
```typescript
221+
import { MyWidget } from './widgets/my-widget';
222+
223+
// Register individual widget
224+
engine.setupWidgets([{
225+
widget: MyWidget,
226+
folder: __dirname + '/widgets/my-widget'
227+
}]);
228+
229+
// Or extend registerDefaultWidgets
230+
class MyWidgetEngine extends WidgetEngine {
231+
registerDefaultWidgets() {
232+
super.registerDefaultWidgets();
233+
234+
this.setupWidgets([{
235+
widget: MyWidget,
236+
folder: __dirname + '/widgets/my-widget'
237+
}]);
238+
}
239+
}
240+
```
241+
242+
## 🧪 Testing
243+
244+
The framework includes comprehensive testing utilities:
245+
246+
### Running Tests
247+
248+
```bash
249+
# Run all tests
250+
npm test
251+
252+
# Run specific widget tests
253+
npm test -- my-widget
254+
255+
# Run with coverage
256+
npm run test:coverage
257+
```
258+
259+
### Test Fixtures
260+
261+
Use the fixture system for consistent testing:
262+
263+
```typescript
264+
import { FixtureLoader } from './test-utils/fixture-loader';
265+
266+
const fixtures = new FixtureLoader();
267+
const { context, template, expected } = fixtures.loadFixture('my-widget', 'basic-example');
268+
269+
const compiledTemplate = handlebars.compile(template);
270+
const result = compiledTemplate(context);
271+
272+
expect(result.trim()).toBe(expected);
273+
```
274+
275+
### Updating Fixtures
276+
277+
Use the fixture update script to regenerate expected outputs:
278+
279+
```bash
280+
npx tsx src/scripts/update-fixtures.ts
281+
```
282+
283+
## 🔍 Architecture
284+
285+
### Core Components
286+
287+
- **WidgetEngine**: Orchestrates widget registration and setup
288+
- **WidgetRegistry**: Manages widget storage and Handlebars partial registration
289+
- **WidgetRenderer**: Handles widget rendering with context validation
290+
- **Widget Helpers**: Global Handlebars helpers available to all widgets
291+
292+
### Helper Functions
293+
294+
The framework provides built-in helpers:
295+
296+
- `eq`, `ne`: Equality comparisons
297+
- `lookup`: Property access
298+
- `json`: JSON stringification
299+
- `kebabToTitleCase`: Convert "api-service" → "Api Service"
300+
- `kebabCase`: Convert "API Service" → "api-service"
301+
- `isObject`, `isArray`: Type checking
302+
- `notEmpty`: Check for non-empty values
303+
- `or`: Logical OR operations
304+
- `currentTimestamp`, `currentDate`: Date utilities
305+
- `instanceOf`: Constructor name checking
306+
- `eachInMap`: Object iteration
307+
308+
### Type Safety
309+
310+
The framework uses TypeScript generics for type-safe widgets:
311+
312+
```typescript
313+
CalmWidget<TContext, TOptions, TViewModel>
314+
```
315+
316+
- `TContext`: Input data type
317+
- `TOptions`: Handlebars options/parameters
318+
- `TViewModel`: Transformed data for template
319+
320+
## 📝 Best Practices
321+
322+
### Widget Design
323+
324+
1. **Keep widgets focused**: Each widget should have a single responsibility
325+
2. **Validate inputs**: Always implement robust `validateContext`
326+
3. **Transform data**: Use `transformToViewModel` to prepare data for templates
327+
4. **Handle errors gracefully**: Provide meaningful error messages
328+
5. **Test thoroughly**: Include unit tests and integration fixtures
329+
330+
### Template Guidelines
331+
332+
1. **Use semantic markup**: Generate clean, readable Markdown
333+
2. **Handle empty data**: Gracefully handle missing or empty inputs
334+
3. **Be consistent**: Follow established patterns from built-in widgets
335+
4. **Optimize performance**: Avoid complex logic in templates
336+
337+
### Testing Strategy
338+
339+
1. **Unit test widget logic**: Test `validateContext` and `transformToViewModel`
340+
2. **Integration test output**: Use fixtures to verify rendered output
341+
3. **Test edge cases**: Handle null, undefined, and malformed data
342+
4. **Maintain fixtures**: Keep expected outputs up to date
343+
344+
## 🤝 Contributing
345+
346+
1. **Create your widget** following the structure above
347+
2. **Add comprehensive tests** including fixtures
348+
3. **Update documentation** if adding new concepts
349+
4. **Follow code style** using the project's ESLint configuration
350+
5. **Test thoroughly** with `npm test`

0 commit comments

Comments
 (0)