@rippling/rippling-sdk 0.2.0-alpha.64 → 0.2.0-alpha.68
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/examples/manifest/dental-practice-management/dental-practice-management.ts +22 -4
- package/examples/manifest/gym-membership-management/gym-membership-management.ts +2 -2
- package/examples/manifest/simple-todo-app/simple-todo-app.ts +1 -1
- package/examples/manifest/todo-app-without-object-list-view/functions/todo-page.ts +487 -0
- package/examples/manifest/todo-app-without-object-list-view/todo-app-without-object-list-view.ts +111 -0
- package/functions.d.mts +5 -5
- package/functions.d.mts.map +1 -1
- package/functions.d.ts +5 -5
- package/functions.d.ts.map +1 -1
- package/functions.js +1 -1
- package/functions.js.map +1 -1
- package/functions.mjs +1 -1
- package/functions.mjs.map +1 -1
- package/lib/manifest/field.d.mts +9 -0
- package/lib/manifest/field.d.mts.map +1 -1
- package/lib/manifest/field.d.ts +9 -0
- package/lib/manifest/field.d.ts.map +1 -1
- package/lib/manifest/field.js +3 -0
- package/lib/manifest/field.js.map +1 -1
- package/lib/manifest/field.mjs +3 -0
- package/lib/manifest/field.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/workers.d.mts +37 -2
- package/resources/workers.d.mts.map +1 -1
- package/resources/workers.d.ts +37 -2
- package/resources/workers.d.ts.map +1 -1
- package/resources/workers.js +2 -2
- package/resources/workers.mjs +2 -2
- package/src/functions.ts +10 -5
- package/src/lib/manifest/field.ts +12 -0
- package/src/resources/workers.ts +43 -2
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -16,6 +16,7 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import {
|
|
18
18
|
App,
|
|
19
|
+
BooleanField,
|
|
19
20
|
Category,
|
|
20
21
|
CustomObject,
|
|
21
22
|
CustomObjectFieldSection,
|
|
@@ -103,6 +104,14 @@ const patientEmailField = new EmailField(patientObj, {
|
|
|
103
104
|
section: patientIdentitySection,
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
const patientActiveField = new BooleanField(patientObj, {
|
|
108
|
+
apiName: 'active_patient__c',
|
|
109
|
+
displayName: 'Active patient',
|
|
110
|
+
description: 'Whether the patient is currently active in the practice',
|
|
111
|
+
required: true,
|
|
112
|
+
section: patientIdentitySection,
|
|
113
|
+
});
|
|
114
|
+
|
|
106
115
|
const patientAllergiesField = new LongTextField(patientObj, {
|
|
107
116
|
apiName: 'allergies_and_alerts__c',
|
|
108
117
|
displayName: 'Allergies & medical alerts',
|
|
@@ -216,7 +225,8 @@ const apptPatientRef = new LookupField(apptObj, {
|
|
|
216
225
|
target: patientObj,
|
|
217
226
|
relatedDataLabel: 'Appointments',
|
|
218
227
|
required: true,
|
|
219
|
-
|
|
228
|
+
lookup_filter_rql_condition: '(active_patient__c == True)',
|
|
229
|
+
description: 'Chart to open for this visit (active patients only)',
|
|
220
230
|
section: apptPatientSection,
|
|
221
231
|
});
|
|
222
232
|
|
|
@@ -378,7 +388,14 @@ new ListViewDef(patientObj, {
|
|
|
378
388
|
viewId: 'view_dental_all_patients',
|
|
379
389
|
name: 'All patients',
|
|
380
390
|
description: 'Full chart list with plan counts',
|
|
381
|
-
fields: [
|
|
391
|
+
fields: [
|
|
392
|
+
'name',
|
|
393
|
+
patientDobField,
|
|
394
|
+
patientPhoneField,
|
|
395
|
+
patientActiveField,
|
|
396
|
+
patientLabelField,
|
|
397
|
+
planCountOnPatient,
|
|
398
|
+
],
|
|
382
399
|
orderBy: 'name',
|
|
383
400
|
sortOrder: 'ASC',
|
|
384
401
|
isPublic: true,
|
|
@@ -449,6 +466,7 @@ new CustomObjectPageLayout(patientObj, {
|
|
|
449
466
|
patientDobField,
|
|
450
467
|
patientPhoneField,
|
|
451
468
|
patientEmailField,
|
|
469
|
+
patientActiveField,
|
|
452
470
|
patientLabelField,
|
|
453
471
|
],
|
|
454
472
|
},
|
|
@@ -570,8 +588,8 @@ new CustomObjectPageLayout(planObj, {
|
|
|
570
588
|
|
|
571
589
|
// ── SDUI object-list tabs ───────────────────────────────────────────────────
|
|
572
590
|
const sduiObjectListDependencies = {
|
|
573
|
-
'@rippling/rippling-sdk': '^0.2.0-alpha.
|
|
574
|
-
'@rippling/pebble-sdui-web': '^0.3
|
|
591
|
+
'@rippling/rippling-sdk': '^0.2.0-alpha.65',
|
|
592
|
+
'@rippling/pebble-sdui-web': '^0.6.3',
|
|
575
593
|
};
|
|
576
594
|
|
|
577
595
|
function loadFunctionCode(filename: string): string {
|
|
@@ -632,8 +632,8 @@ new CustomObjectPageLayout(enrollmentObj, {
|
|
|
632
632
|
|
|
633
633
|
// ── SDUI object-list tabs ───────────────────────────────────────────────────
|
|
634
634
|
const sduiObjectListDependencies = {
|
|
635
|
-
'@rippling/rippling-sdk': '^0.2.0-alpha.
|
|
636
|
-
'@rippling/pebble-sdui-web': '^0.3
|
|
635
|
+
'@rippling/rippling-sdk': '^0.2.0-alpha.65',
|
|
636
|
+
'@rippling/pebble-sdui-web': '^0.6.3',
|
|
637
637
|
};
|
|
638
638
|
|
|
639
639
|
function loadFunctionCode(filename: string): string {
|
|
@@ -134,7 +134,7 @@ const todoListFn = new ManifestFunction(manifest, {
|
|
|
134
134
|
name: 'renderTodos',
|
|
135
135
|
description: 'SDUI ObjectListView for todo records',
|
|
136
136
|
code: fs.readFileSync(path.join(__dirname, 'functions', 'todo-list.ts'), 'utf-8'),
|
|
137
|
-
dependencies: { '@rippling/rippling-sdk': '^0.2.0-alpha.
|
|
137
|
+
dependencies: { '@rippling/rippling-sdk': '^0.2.0-alpha.65', '@rippling/pebble-sdui-web': '^0.6.3' },
|
|
138
138
|
isAsync: false,
|
|
139
139
|
});
|
|
140
140
|
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Custom SDUI todo page backed by `todo__c` records.
|
|
3
|
+
*
|
|
4
|
+
* This function handles both the initial page render and follow-up UI actions:
|
|
5
|
+
* - `init` lists todos for the current `context.function.role_id` and returns
|
|
6
|
+
* a JSON-stringified SDUI spec in `body.spec`.
|
|
7
|
+
* - `addTodo`, `toggleTodo`, and `clearCompleted` mutate Custom Object records,
|
|
8
|
+
* then return the fresh `TodoListState` written into `/todoList` by
|
|
9
|
+
* `responsePath`.
|
|
10
|
+
*
|
|
11
|
+
* In `triggerFunction`, server-bound action params live under `inputs` so
|
|
12
|
+
* repeated item values like `todo.$('id')` resolve before reaching
|
|
13
|
+
* `event['parameters']`. Client-side controls such as `functionId`,
|
|
14
|
+
* `responsePath`, `onSuccess`, and `onError` stay top-level.
|
|
15
|
+
*
|
|
16
|
+
* `/isMutating` is a shared loading flag: it disables every action while an API
|
|
17
|
+
* call is in flight and shows a small loading message so disabled controls have
|
|
18
|
+
* visible context.
|
|
19
|
+
*/
|
|
20
|
+
import { createSpec, equals, neq, or_, type SpecContext } from '@rippling/pebble-sdui-web';
|
|
21
|
+
import {
|
|
22
|
+
Button,
|
|
23
|
+
Card,
|
|
24
|
+
Checkbox,
|
|
25
|
+
Field,
|
|
26
|
+
HStack,
|
|
27
|
+
PageContent,
|
|
28
|
+
Text,
|
|
29
|
+
TextInput,
|
|
30
|
+
VStack,
|
|
31
|
+
render,
|
|
32
|
+
setState,
|
|
33
|
+
triggerFunction,
|
|
34
|
+
} from '@rippling/pebble-sdui-web/catalogs/custom-apps';
|
|
35
|
+
import RipplingSDK, { FunctionContext, FunctionEvent, FunctionResponse } from '@rippling/rippling-sdk';
|
|
36
|
+
import type { CustomObjectDataRow } from '@rippling/rippling-sdk/resources/custom-objects/records';
|
|
37
|
+
|
|
38
|
+
const OBJECT_API_NAME = 'todo__c';
|
|
39
|
+
|
|
40
|
+
type Todo = {
|
|
41
|
+
id: string;
|
|
42
|
+
text: string;
|
|
43
|
+
completed: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type TodoListState = {
|
|
47
|
+
todos: Todo[];
|
|
48
|
+
isEmpty: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type TodoAppState = {
|
|
52
|
+
newTodoText: string;
|
|
53
|
+
todoList: TodoListState;
|
|
54
|
+
isMutating: boolean;
|
|
55
|
+
errorMessage: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type TodoSpec = SpecContext<TodoAppState>;
|
|
59
|
+
|
|
60
|
+
function createClient(context: FunctionContext): RipplingSDK {
|
|
61
|
+
return new RipplingSDK({
|
|
62
|
+
bearerToken: context.env.rippling_user_bearer_token,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toTodo(record: CustomObjectDataRow): Todo {
|
|
67
|
+
return {
|
|
68
|
+
id: record.id,
|
|
69
|
+
text: record.name,
|
|
70
|
+
completed: record['completed__c'] === true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function listTodos(client: RipplingSDK, roleId: string): Promise<Todo[]> {
|
|
75
|
+
const response = await client.customObjects.records.query(OBJECT_API_NAME, {
|
|
76
|
+
limit: 100,
|
|
77
|
+
query: `created_by = "${roleId}"`, // RQL query to filter todos by the current role
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return (response.results ?? []).map(toTodo);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function todoListState(todos: Todo[]): TodoListState {
|
|
84
|
+
return {
|
|
85
|
+
todos,
|
|
86
|
+
isEmpty: todos.length === 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function todoState(todos: Todo[]): TodoAppState {
|
|
91
|
+
return {
|
|
92
|
+
newTodoText: '',
|
|
93
|
+
todoList: todoListState(todos),
|
|
94
|
+
isMutating: false,
|
|
95
|
+
errorMessage: '',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ok(body: Record<string, unknown> | unknown[] = {}): FunctionResponse {
|
|
100
|
+
return new FunctionResponse({
|
|
101
|
+
body: body as Record<string, unknown>,
|
|
102
|
+
statusCode: 200,
|
|
103
|
+
headers: {},
|
|
104
|
+
message: '',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function badRequest(message: string): FunctionResponse {
|
|
109
|
+
return new FunctionResponse({
|
|
110
|
+
body: { error: message },
|
|
111
|
+
statusCode: 400,
|
|
112
|
+
headers: {},
|
|
113
|
+
message,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function serverError(error: unknown): FunctionResponse {
|
|
118
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
119
|
+
|
|
120
|
+
return new FunctionResponse({
|
|
121
|
+
body: { error: message },
|
|
122
|
+
statusCode: 500,
|
|
123
|
+
headers: {},
|
|
124
|
+
message,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stringParam(params: Record<string, unknown>, key: string): string {
|
|
129
|
+
const value = params[key];
|
|
130
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A single mutation flag disables all actions and drives the visible loading text.
|
|
134
|
+
function setMutationState(isMutating: boolean) {
|
|
135
|
+
return setState({
|
|
136
|
+
statePath: '/isMutating',
|
|
137
|
+
value: isMutating,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildAddTodoSection(spec: TodoSpec, functionId: string) {
|
|
142
|
+
return VStack({
|
|
143
|
+
id: 'addTodoSection',
|
|
144
|
+
props: {},
|
|
145
|
+
children: [
|
|
146
|
+
Field({
|
|
147
|
+
id: 'todoInputField',
|
|
148
|
+
props: {
|
|
149
|
+
label: 'New Todo',
|
|
150
|
+
},
|
|
151
|
+
children: [
|
|
152
|
+
TextInput({
|
|
153
|
+
id: 'todoInput',
|
|
154
|
+
props: {
|
|
155
|
+
name: 'newTodo',
|
|
156
|
+
placeholder: 'Enter a new todo item',
|
|
157
|
+
value: spec.$bindState('/newTodoText'),
|
|
158
|
+
isDisabled: spec.$state('/isMutating'),
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
Button({
|
|
164
|
+
id: 'addButton',
|
|
165
|
+
props: {
|
|
166
|
+
label: 'Add Todo',
|
|
167
|
+
appearance: 'PRIMARY',
|
|
168
|
+
isFluid: true,
|
|
169
|
+
isDisabled: spec.$cond(
|
|
170
|
+
or_(equals(spec.$state('/newTodoText'), ''), equals(spec.$state('/isMutating'), true)),
|
|
171
|
+
true,
|
|
172
|
+
false,
|
|
173
|
+
),
|
|
174
|
+
},
|
|
175
|
+
on: {
|
|
176
|
+
press: [
|
|
177
|
+
setMutationState(true),
|
|
178
|
+
triggerFunction(
|
|
179
|
+
{
|
|
180
|
+
functionId,
|
|
181
|
+
inputs: {
|
|
182
|
+
action: 'addTodo',
|
|
183
|
+
text: spec.$state('/newTodoText'),
|
|
184
|
+
},
|
|
185
|
+
responsePath: '/todoList',
|
|
186
|
+
} as any,
|
|
187
|
+
{
|
|
188
|
+
onSuccess: { set: { '/newTodoText': '', '/errorMessage': '', '/isMutating': false } },
|
|
189
|
+
onError: { set: { '/errorMessage': 'Could not add todo. Try again.', '/isMutating': false } },
|
|
190
|
+
},
|
|
191
|
+
),
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildTodoItems(spec: TodoSpec, functionId: string) {
|
|
200
|
+
const [todoRepeat, todo] = spec.repeat('/todoList/todos', 'id');
|
|
201
|
+
|
|
202
|
+
return VStack({
|
|
203
|
+
id: 'todoItems',
|
|
204
|
+
repeat: todoRepeat,
|
|
205
|
+
props: {
|
|
206
|
+
gap: 'space200',
|
|
207
|
+
},
|
|
208
|
+
children: [
|
|
209
|
+
HStack({
|
|
210
|
+
id: 'todoItem',
|
|
211
|
+
props: {
|
|
212
|
+
gap: 'space300',
|
|
213
|
+
align: 'center',
|
|
214
|
+
padding: 'space300',
|
|
215
|
+
backgroundColor: 'colorSurfaceContainer',
|
|
216
|
+
radius: 'shapeCornerMd',
|
|
217
|
+
},
|
|
218
|
+
children: [
|
|
219
|
+
Checkbox({
|
|
220
|
+
id: 'todoCheckbox',
|
|
221
|
+
props: {
|
|
222
|
+
name: 'completed',
|
|
223
|
+
value: todo.$bind('completed'),
|
|
224
|
+
isDisabled: spec.$state('/isMutating'),
|
|
225
|
+
},
|
|
226
|
+
on: {
|
|
227
|
+
change: [
|
|
228
|
+
setMutationState(true),
|
|
229
|
+
triggerFunction(
|
|
230
|
+
{
|
|
231
|
+
functionId,
|
|
232
|
+
inputs: {
|
|
233
|
+
action: 'toggleTodo',
|
|
234
|
+
todoId: todo.$('id'),
|
|
235
|
+
},
|
|
236
|
+
responsePath: '/todoList',
|
|
237
|
+
} as any,
|
|
238
|
+
{
|
|
239
|
+
onSuccess: { set: { '/errorMessage': '', '/isMutating': false } },
|
|
240
|
+
onError: {
|
|
241
|
+
set: {
|
|
242
|
+
'/errorMessage': 'Could not update todo. Refresh and try again.',
|
|
243
|
+
'/isMutating': false,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
),
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
Text({
|
|
252
|
+
id: 'todoText',
|
|
253
|
+
props: {
|
|
254
|
+
text: todo.$('text'),
|
|
255
|
+
decoration: spec.$cond(equals(todo.$('completed'), true), 'line-through', 'none'),
|
|
256
|
+
color: spec.$cond(equals(todo.$('completed'), true), 'colorOnSurfaceVariant', 'colorOnSurface'),
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
],
|
|
260
|
+
}),
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildTodoList(spec: TodoSpec, functionId: string) {
|
|
266
|
+
return VStack({
|
|
267
|
+
id: 'todoList',
|
|
268
|
+
props: {
|
|
269
|
+
gap: 'space300',
|
|
270
|
+
},
|
|
271
|
+
children: [
|
|
272
|
+
HStack({
|
|
273
|
+
id: 'todoListHeader',
|
|
274
|
+
props: {
|
|
275
|
+
align: 'center',
|
|
276
|
+
justify: 'space-between',
|
|
277
|
+
gap: 'space300',
|
|
278
|
+
},
|
|
279
|
+
children: [
|
|
280
|
+
Text({
|
|
281
|
+
id: 'todoListTitle',
|
|
282
|
+
props: {
|
|
283
|
+
text: 'Your Todos',
|
|
284
|
+
typestyle: 'typestyleTitleSmall600',
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
Button({
|
|
288
|
+
id: 'clearCompletedButton',
|
|
289
|
+
props: {
|
|
290
|
+
label: 'Clear completed',
|
|
291
|
+
appearance: 'OUTLINE',
|
|
292
|
+
size: 'S',
|
|
293
|
+
isDisabled: spec.$cond(
|
|
294
|
+
or_(equals(spec.$state('/isMutating'), true), equals(spec.$state('/todoList/isEmpty'), true)),
|
|
295
|
+
true,
|
|
296
|
+
false,
|
|
297
|
+
),
|
|
298
|
+
},
|
|
299
|
+
on: {
|
|
300
|
+
press: [
|
|
301
|
+
setMutationState(true),
|
|
302
|
+
triggerFunction(
|
|
303
|
+
{
|
|
304
|
+
functionId,
|
|
305
|
+
inputs: {
|
|
306
|
+
action: 'clearCompleted',
|
|
307
|
+
},
|
|
308
|
+
responsePath: '/todoList',
|
|
309
|
+
} as any,
|
|
310
|
+
{
|
|
311
|
+
onSuccess: { set: { '/errorMessage': '', '/isMutating': false } },
|
|
312
|
+
onError: {
|
|
313
|
+
set: {
|
|
314
|
+
'/errorMessage': 'Could not clear completed todos. Try again.',
|
|
315
|
+
'/isMutating': false,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
),
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
],
|
|
324
|
+
}),
|
|
325
|
+
Text({
|
|
326
|
+
id: 'emptyTodos',
|
|
327
|
+
visible: equals(spec.$state('/todoList/isEmpty'), true),
|
|
328
|
+
props: {
|
|
329
|
+
text: 'No todos yet.',
|
|
330
|
+
color: 'colorOnSurfaceVariant',
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
buildTodoItems(spec, functionId),
|
|
334
|
+
],
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildError(spec: TodoSpec) {
|
|
339
|
+
return Text({
|
|
340
|
+
id: 'todoError',
|
|
341
|
+
visible: neq(spec.$state('/errorMessage'), ''),
|
|
342
|
+
props: {
|
|
343
|
+
text: spec.$state('/errorMessage'),
|
|
344
|
+
color: 'colorError',
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildLoading(spec: TodoSpec) {
|
|
350
|
+
return Text({
|
|
351
|
+
id: 'todoLoading',
|
|
352
|
+
// Keep disabled controls explainable while a triggerFunction call is in flight.
|
|
353
|
+
visible: equals(spec.$state('/isMutating'), true),
|
|
354
|
+
props: {
|
|
355
|
+
text: 'Updating todos...',
|
|
356
|
+
color: 'colorOnSurfaceVariant',
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function buildTodoCard(spec: TodoSpec, functionId: string) {
|
|
362
|
+
return Card({
|
|
363
|
+
id: 'todoCard',
|
|
364
|
+
props: {},
|
|
365
|
+
children: [
|
|
366
|
+
VStack({
|
|
367
|
+
id: 'todoContent',
|
|
368
|
+
props: {
|
|
369
|
+
gap: 'space400',
|
|
370
|
+
padding: 'space400',
|
|
371
|
+
},
|
|
372
|
+
children: [
|
|
373
|
+
buildAddTodoSection(spec, functionId),
|
|
374
|
+
buildError(spec),
|
|
375
|
+
buildLoading(spec),
|
|
376
|
+
buildTodoList(spec, functionId),
|
|
377
|
+
],
|
|
378
|
+
}),
|
|
379
|
+
],
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function renderSpec(state: TodoAppState, functionId: string) {
|
|
384
|
+
const spec = createSpec<TodoAppState>({ state });
|
|
385
|
+
const root = PageContent({
|
|
386
|
+
id: 'todoApp',
|
|
387
|
+
props: {
|
|
388
|
+
size: '776',
|
|
389
|
+
alignment: 'CENTER',
|
|
390
|
+
},
|
|
391
|
+
children: [buildTodoCard(spec, functionId)],
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return render({ root, state });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function addTodo(
|
|
398
|
+
client: RipplingSDK,
|
|
399
|
+
params: Record<string, unknown>,
|
|
400
|
+
roleId: string,
|
|
401
|
+
): Promise<TodoListState> {
|
|
402
|
+
const text = stringParam(params, 'text');
|
|
403
|
+
|
|
404
|
+
if (!text) {
|
|
405
|
+
throw new Error('Todo text is required');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
await client.customObjects.records.create(OBJECT_API_NAME, {
|
|
409
|
+
name: text,
|
|
410
|
+
completed__c: false,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return todoListState(await listTodos(client, roleId));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function toggleTodo(
|
|
417
|
+
client: RipplingSDK,
|
|
418
|
+
params: Record<string, unknown>,
|
|
419
|
+
roleId: string,
|
|
420
|
+
): Promise<TodoListState> {
|
|
421
|
+
const todoId = stringParam(params, 'todoId');
|
|
422
|
+
|
|
423
|
+
if (!todoId) {
|
|
424
|
+
throw new Error('todoId must be a todo record ID');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const todo = toTodo(await client.customObjects.records.retrieve(OBJECT_API_NAME, todoId));
|
|
428
|
+
await client.customObjects.records.update(OBJECT_API_NAME, todoId, {
|
|
429
|
+
completed__c: !todo.completed,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return todoListState(await listTodos(client, roleId));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function clearCompleted(client: RipplingSDK, roleId: string): Promise<TodoListState> {
|
|
436
|
+
const todos = await listTodos(client, roleId);
|
|
437
|
+
const completedTodoIds = todos.filter((todo) => todo.completed).map((todo) => todo.id);
|
|
438
|
+
|
|
439
|
+
if (completedTodoIds.length === 0) {
|
|
440
|
+
return todoListState(todos);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await client.customObjects.records.bulkDelete(OBJECT_API_NAME, {
|
|
444
|
+
all_or_nothing: false,
|
|
445
|
+
rows_to_delete: completedTodoIds,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return todoListState(await listTodos(client, roleId));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function onRipplingEvent(
|
|
452
|
+
event: FunctionEvent,
|
|
453
|
+
context: FunctionContext,
|
|
454
|
+
): Promise<FunctionResponse> {
|
|
455
|
+
const params = (event['parameters'] ?? {}) as Record<string, unknown>;
|
|
456
|
+
const action = typeof params['action'] === 'string' ? params['action'] : 'init';
|
|
457
|
+
const roleId = context.function.role_id;
|
|
458
|
+
if (!roleId) {
|
|
459
|
+
return badRequest('Role ID is required');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const client = createClient(context);
|
|
464
|
+
|
|
465
|
+
if (action === 'init') {
|
|
466
|
+
const functionId = context.function.function_id;
|
|
467
|
+
const todos = await listTodos(client, roleId);
|
|
468
|
+
return ok({ spec: JSON.stringify(renderSpec(todoState(todos), functionId)) });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (action === 'addTodo') {
|
|
472
|
+
return ok(await addTodo(client, params, roleId));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (action === 'toggleTodo') {
|
|
476
|
+
return ok(await toggleTodo(client, params, roleId));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (action === 'clearCompleted') {
|
|
480
|
+
return ok(await clearCompleted(client, roleId));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return badRequest(`Unknown action: ${action}`);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
return serverError(error);
|
|
486
|
+
}
|
|
487
|
+
}
|
package/examples/manifest/todo-app-without-object-list-view/todo-app-without-object-list-view.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Todo App Without ObjectListView — custom SDUI backed by Custom Object records.
|
|
3
|
+
*
|
|
4
|
+
* This example intentionally does not render ObjectListView. The SDUI page reads
|
|
5
|
+
* and writes `todo__c` records from the function, then renders its own
|
|
6
|
+
* todo list with TextInput, Button, Checkbox, and layout components.
|
|
7
|
+
*
|
|
8
|
+
* todo__c <- name, completed__c
|
|
9
|
+
*
|
|
10
|
+
* Run:
|
|
11
|
+
* yarn tsn examples/manifest/todo-app-without-object-list-view/todo-app-without-object-list-view.ts \
|
|
12
|
+
* > examples/manifest/todo-app-without-object-list-view/manifest.json
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import {
|
|
18
|
+
App,
|
|
19
|
+
BooleanField,
|
|
20
|
+
Category,
|
|
21
|
+
CustomObject,
|
|
22
|
+
CustomObjectFieldSection,
|
|
23
|
+
CustomObjectPageLayout,
|
|
24
|
+
ManifestBuilder,
|
|
25
|
+
ManifestFunction,
|
|
26
|
+
SduiPage,
|
|
27
|
+
} from '@rippling/rippling-sdk/lib/manifest';
|
|
28
|
+
|
|
29
|
+
const manifest = new ManifestBuilder({
|
|
30
|
+
key: 'todo_app_without_object_list_view',
|
|
31
|
+
name: 'Todo App Without ObjectListView',
|
|
32
|
+
description: 'A todo app that renders custom SDUI and persists changes to Custom Object records',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const todoCategory = new Category(manifest, {
|
|
36
|
+
apiName: 'custom_todo_app__c',
|
|
37
|
+
name: 'Todo App',
|
|
38
|
+
description: 'Custom todo app records',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const todoObj = new CustomObject(manifest, {
|
|
42
|
+
apiName: 'todo__c',
|
|
43
|
+
name: 'Todo',
|
|
44
|
+
pluralLabel: 'Todos',
|
|
45
|
+
category: todoCategory,
|
|
46
|
+
description: 'A todo item rendered by a custom SDUI page',
|
|
47
|
+
icon: { emoji: '✅' },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const detailsSection = new CustomObjectFieldSection(todoObj, {
|
|
51
|
+
name: 'Details',
|
|
52
|
+
sectionId: 'sec_custom_todo_details',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const completedField = new BooleanField(todoObj, {
|
|
56
|
+
apiName: 'completed__c',
|
|
57
|
+
displayName: 'Completed',
|
|
58
|
+
description: 'Whether the todo item is complete',
|
|
59
|
+
section: detailsSection,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
new CustomObjectPageLayout(todoObj, {
|
|
63
|
+
apiName: 'default',
|
|
64
|
+
name: 'Todo',
|
|
65
|
+
layoutId: 'layout_custom_todo',
|
|
66
|
+
tabEdits: {
|
|
67
|
+
newTabs: [
|
|
68
|
+
{
|
|
69
|
+
key: 'todo',
|
|
70
|
+
name: 'Todo',
|
|
71
|
+
type: 'tab_with_sections',
|
|
72
|
+
sections: [
|
|
73
|
+
{
|
|
74
|
+
name: 'Details',
|
|
75
|
+
type: 'fields_section',
|
|
76
|
+
section: detailsSection,
|
|
77
|
+
fields: [{ field: 'name', editable: true }, completedField],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
systemTabEdits: {},
|
|
83
|
+
tabsOrder: ['todo'],
|
|
84
|
+
},
|
|
85
|
+
visibilityConditions: {},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const todoPageFn = new ManifestFunction(manifest, {
|
|
89
|
+
apiName: 'custom_todo_page_fn',
|
|
90
|
+
name: 'renderTodoPage',
|
|
91
|
+
description: 'Custom SDUI todo page backed by Custom Object record APIs',
|
|
92
|
+
code: fs.readFileSync(path.join(__dirname, 'functions', 'todo-page.ts'), 'utf-8'),
|
|
93
|
+
dependencies: { '@rippling/rippling-sdk': '^0.2.0-alpha.65', '@rippling/pebble-sdui-web': '^0.6.3' },
|
|
94
|
+
isAsync: false,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const todoPage = new SduiPage(manifest, {
|
|
98
|
+
sduiPageId: 'custom_todo_page',
|
|
99
|
+
name: 'Todos',
|
|
100
|
+
function: todoPageFn,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
new App(manifest, {
|
|
104
|
+
apiName: 'todo_app_without_object_list_view',
|
|
105
|
+
name: 'Todo App Without ObjectListView',
|
|
106
|
+
description: 'Todo app with a fully custom SDUI page and Custom Object persistence',
|
|
107
|
+
appType: 'custom',
|
|
108
|
+
pages: [todoPage],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(manifest.preview());
|
package/functions.d.mts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
export interface FunctionResponseOptions {
|
|
2
|
-
body?:
|
|
1
|
+
export interface FunctionResponseOptions<T = Record<string, any>> {
|
|
2
|
+
body?: T;
|
|
3
3
|
statusCode?: number;
|
|
4
4
|
headers?: Record<string, string>;
|
|
5
5
|
message?: string;
|
|
6
6
|
}
|
|
7
|
-
export declare class FunctionResponse {
|
|
8
|
-
body:
|
|
7
|
+
export declare class FunctionResponse<T = Record<string, any>> {
|
|
8
|
+
body: T;
|
|
9
9
|
statusCode: number;
|
|
10
10
|
headers: Record<string, string>;
|
|
11
11
|
message: string;
|
|
12
|
-
constructor({ body, statusCode, headers, message }?: FunctionResponseOptions);
|
|
12
|
+
constructor({ body, statusCode, headers, message, }?: FunctionResponseOptions<T>);
|
|
13
13
|
}
|
|
14
14
|
export type FunctionEvent = Record<string, any>;
|
|
15
15
|
export type DurableDuration = number | string;
|