@api-client/ui 0.6.4 → 0.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.js",
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
2
  import { nothing, TemplateResult } from 'lit'
3
- import { ActivityLifecycle, IntentResult, type Intent } from './ActivityManager.js'
3
+ import { ActivityLifecycle, IntentResult, type Intent, type ResolvedIntent } from './ActivityManager.js'
4
4
  import { FragmentManager } from './FragmentManager.js'
5
5
  import type { Fragment } from './Fragment.js'
6
6
  import type { Application, UpdateRequest } from './Application.js'
@@ -8,6 +8,13 @@ import { bound } from '../decorators/bound.js'
8
8
  import type { ActivityDetail, ActivityWithResultDetail } from '../events/IntentEvents.js'
9
9
  import { EventTypes } from '../events/EventTypes.js'
10
10
 
11
+ export interface ActivityResult {
12
+ requestCode: number
13
+ resultCode: IntentResult
14
+ data?: unknown
15
+ intent: Intent
16
+ }
17
+
11
18
  /**
12
19
  * ## Activity
13
20
  *
@@ -84,9 +91,6 @@ export class Activity extends EventTarget {
84
91
  return this.exitCode
85
92
  }
86
93
 
87
- /** Tracks pending request codes for activities started for result. */
88
- protected pendingRequestCodes: number[] = []
89
-
90
94
  /**
91
95
  * Constructs a new Activity.
92
96
  * @param parent The parent application instance.
@@ -97,6 +101,14 @@ export class Activity extends EventTarget {
97
101
  this.manager = new FragmentManager(this)
98
102
  }
99
103
 
104
+ /**
105
+ * Returns the current activity instance.
106
+ * @returns This activity.
107
+ */
108
+ getActivity(): Activity {
109
+ return this
110
+ }
111
+
100
112
  /**
101
113
  * Checks if the activity is in the `Destroyed` state.
102
114
  * @returns `true` if destroyed, `false` otherwise.
@@ -282,25 +294,28 @@ export class Activity extends EventTarget {
282
294
  * await this.startActivity({ action: 'login' });
283
295
  * ```
284
296
  */
285
- startActivity(intent: Intent): Promise<void> {
286
- return this.parent.manager.startActivity(intent)
297
+ async startActivity(intent: Intent): Promise<Activity> {
298
+ const { manager } = this.getApplication()
299
+ return manager.startActivity(intent)
287
300
  }
288
301
 
289
302
  /**
290
303
  * Starts a new activity for a result.
304
+ * @template T The type of the result data expected.
291
305
  * @param intent The intent to start.
292
- * @returns The request code for the started activity.
306
+ * @returns A promise that resolves when the started activity finishes, returning the result Intent with
307
+ * data of type T.
293
308
  * @example
294
309
  * ```typescript
295
- * const code = await this.startActivityForResult({ action: 'pickFile' });
310
+ * const resultIntent = await this.startActivityForResult<{ userId: string }>({ action: 'pickUser' });
311
+ * if (resultIntent.resultCode === IntentResult.RESULT_OK) {
312
+ * console.log(resultIntent.data?.userId);
313
+ * }
296
314
  * ```
297
315
  */
298
- async startActivityForResult(intent: Intent): Promise<number> {
316
+ async startActivityForResult<T>(intent: Intent): Promise<ResolvedIntent<T | undefined>> {
299
317
  const { manager } = this.getApplication()
300
- const code = manager.createRequestCode()
301
- this.pendingRequestCodes.push(code)
302
- await manager.startActivityForResult(intent, code)
303
- return code
318
+ return manager.startActivityForResult(intent)
304
319
  }
305
320
 
306
321
  /**
@@ -317,57 +332,6 @@ export class Activity extends EventTarget {
317
332
  this.resultData = data
318
333
  }
319
334
 
320
- /**
321
- * Called when an activity you launched exits, giving you the request code, result code, and intent.
322
- * Override to handle results from started activities.
323
- * @param requestCode The request code.
324
- * @param resultCode The result code.
325
- * @param intent The intent that was used to start the activity.
326
- * @example
327
- * ```typescript
328
- * async onActivityResult(requestCode, resultCode, intent) {
329
- * if (resultCode === IntentResult.RESULT_OK) {
330
- * // handle result
331
- * }
332
- * }
333
- * ```
334
- */
335
- async onActivityResult(requestCode: number, resultCode: IntentResult, intent: Intent): Promise<void> {
336
- const index = this.pendingRequestCodes.indexOf(requestCode)
337
- if (index !== -1) {
338
- this.pendingRequestCodes.splice(index, 1)
339
- }
340
- // First we check whether any of the fragments has the code.
341
- const fragment = this.manager.findByRequestCode(requestCode)
342
- if (fragment) {
343
- return fragment.onActivityResult(requestCode, resultCode, intent)
344
- }
345
- if (this.constructor === Activity) {
346
- // eslint-disable-next-line no-console
347
- console.info(
348
- `Activity#onActivityResult not implemented. Request code: ${requestCode}, result code: ${resultCode}`,
349
- intent
350
- )
351
- }
352
- }
353
-
354
- /**
355
- * Returns the current activity instance.
356
- * @returns This activity.
357
- */
358
- getActivity(): Activity {
359
- return this
360
- }
361
-
362
- /**
363
- * Checks if this activity initiated the activity for result with the given code.
364
- * @param code The request code.
365
- * @returns `true` if the code is pending, `false` otherwise.
366
- */
367
- hasRequestCode(code: number): boolean {
368
- return this.pendingRequestCodes.includes(code)
369
- }
370
-
371
335
  /**
372
336
  * Gets the result data set by `setResult`.
373
337
  * @returns The result data or `undefined`.
@@ -392,7 +356,8 @@ export class Activity extends EventTarget {
392
356
  async handleIntentEvent(event: CustomEvent<ActivityDetail | ActivityWithResultDetail>): Promise<void> {
393
357
  if (event.type === EventTypes.Intent.startActivityForResult) {
394
358
  const info = event.detail as ActivityWithResultDetail
395
- await this.startActivityForResult(info.intent)
359
+ const result = await this.startActivityForResult(info.intent)
360
+ info.onResult(result.result, result)
396
361
  } else if (event.type === EventTypes.Intent.startActivity) {
397
362
  await this.startActivity(event.detail.intent)
398
363
  } else {
@@ -1,6 +1,6 @@
1
1
  import type { Activity } from './Activity.js'
2
2
  import type { Application } from './Application.js'
3
- import { navigateScreen } from './ApplicationRoute.js'
3
+ // import { navigateScreen } from './ApplicationRoute.js'
4
4
 
5
5
  export enum ActivityLifecycle {
6
6
  /**
@@ -59,9 +59,22 @@ export enum IntentFlags {
59
59
  }
60
60
 
61
61
  export interface Intent<T = unknown> {
62
+ /**
63
+ * The action name the activity is registered for.
64
+ */
62
65
  action: string
66
+ /**
67
+ * The data passed to the activity.
68
+ */
63
69
  data?: T
70
+ /**
71
+ * The category of the activity.
72
+ * Optional and currently not used.
73
+ */
64
74
  category?: string[]
75
+ /**
76
+ * The flags that control the behavior of the activity.
77
+ */
65
78
  flags?: number
66
79
  /**
67
80
  * The request code used to distinguish between different activities.
@@ -69,6 +82,17 @@ export interface Intent<T = unknown> {
69
82
  requestCode?: number
70
83
  }
71
84
 
85
+ /**
86
+ * An intent returned by an activity after the activity result is ready.
87
+ * This object is only created when the activity is started with the `ForResult` flag.
88
+ */
89
+ export interface ResolvedIntent<T = unknown> extends Intent<T> {
90
+ /**
91
+ * The result code returned by the activity.
92
+ */
93
+ result: IntentResult
94
+ }
95
+
72
96
  export enum IntentResult {
73
97
  RESULT_CANCELED,
74
98
  RESULT_OK,
@@ -134,6 +158,11 @@ export class ActivityManager {
134
158
  */
135
159
  private activityClasses = new Map<string, typeof Activity>()
136
160
 
161
+ /**
162
+ * Stores the resolvers for activities started for result.
163
+ */
164
+ private resultResolvers = new Map<Intent, (intent: ResolvedIntent) => void>()
165
+
137
166
  /**
138
167
  * Represents the activity stack. It is used to manage
139
168
  * the UI state after calling the back action.
@@ -184,15 +213,6 @@ export class ActivityManager {
184
213
  this.activityClasses.set(action, activityClass as typeof Activity)
185
214
  }
186
215
 
187
- // protected reflectEvent<T extends Event>(event: T): T {
188
- // const copy = Reflect.construct(event.constructor, [event.type, event])
189
- // const dispatched = this.#parent.events.dispatchEvent(copy)
190
- // if (!dispatched) {
191
- // event.preventDefault()
192
- // }
193
- // return copy
194
- // }
195
-
196
216
  /**
197
217
  * Creates an activity and pushes it into the stack, but it does not initialize any of the lifecycle methods.
198
218
  * It is a way to put an activity at some place of the stack,
@@ -210,21 +230,21 @@ export class ActivityManager {
210
230
  this.activityStack.push(stackEntry)
211
231
  }
212
232
 
213
- setupRoute(intent: Intent): void {
214
- if (intent.data) {
215
- const typed = intent.data as { uri?: string }
216
- if (typed.uri) {
217
- navigateScreen(typed.uri)
218
- }
219
- }
220
- }
233
+ // setupRoute(intent: Intent): void {
234
+ // if (intent.data) {
235
+ // const typed = intent.data as { uri?: string }
236
+ // if (typed.uri) {
237
+ // navigateScreen(typed.uri)
238
+ // }
239
+ // }
240
+ // }
221
241
 
222
242
  /**
223
243
  * Starts a new activity and brings it to the foreground.
224
244
  *
225
245
  * @param intent The intent that created this activity.
226
246
  */
227
- async startActivity(intent: Intent): Promise<void> {
247
+ async startActivity(intent: Intent): Promise<Activity> {
228
248
  const { currentActivity, activityStack } = this
229
249
  const lastIndex = activityStack.findLastIndex((entry) => entry.action === intent.action)
230
250
  // if the activity is in the history list, we bring it up and update intent.
@@ -239,9 +259,9 @@ export class ActivityManager {
239
259
  })
240
260
  await info.activity.onNewIntent(intent)
241
261
  // TODO: Check if the activity is destroyed.
242
- this.setupRoute(intent)
262
+ // this.setupRoute(intent)
243
263
  await this.updateCurrentActivity(info.activity, false)
244
- return
264
+ return info.activity
245
265
  }
246
266
  const activity = this.buildActivity(intent)
247
267
  if (currentActivity) {
@@ -263,16 +283,17 @@ export class ActivityManager {
263
283
  await activity.onCreate(intent)
264
284
  if (this.isDestroyed(activity)) {
265
285
  // the activity finished and the manager already took care of this situation.
266
- return
286
+ return activity
267
287
  }
268
288
  activity.lifecycle = ActivityLifecycle.Created
269
289
 
270
290
  await this.updateCurrentActivity(activity, !!isModal)
271
291
  if (this.isDestroyed(activity)) {
272
292
  // the activity finished and the manager already took care of this situation.
273
- return
293
+ return activity
274
294
  }
275
- this.setupRoute(intent)
295
+ // this.setupRoute(intent)
296
+ return activity
276
297
  }
277
298
 
278
299
  private buildActivity(intent: Intent): Activity {
@@ -293,12 +314,20 @@ export class ActivityManager {
293
314
 
294
315
  /**
295
316
  * Starts an activity that should return a result to the calling activity.
296
- * The result should be a unique number across the application.
317
+ *
318
+ * @template T The type of the result data expected.
297
319
  * @param intent The intent to start the activity.
298
- * @param requestCode The request code used to match the activity result.
299
- */
300
- async startActivityForResult(intent: Intent, requestCode: number): Promise<void> {
301
- intent.requestCode = requestCode
320
+ * @returns A promise that resolves when the started activity finishes, returning the result Intent with
321
+ * data of type T.
322
+ * @example
323
+ * ```typescript
324
+ * const resultIntent = await mgr.startActivityForResult<{ userId: string }>({ action: 'pickUser' });
325
+ * if (resultIntent.resultCode === IntentResult.RESULT_OK) {
326
+ * console.log(resultIntent.data?.userId);
327
+ * }
328
+ * ```
329
+ */
330
+ async startActivityForResult<T>(intent: Intent): Promise<ResolvedIntent<T | undefined>> {
302
331
  const byReference = (intent.flags ?? 0) & IntentFlags.ByReference
303
332
  const shallowCopy = { ...intent }
304
333
  const data = shallowCopy.data
@@ -314,7 +343,14 @@ export class ActivityManager {
314
343
  } else {
315
344
  deepCopy.flags = IntentFlags.ForResult
316
345
  }
317
- await this.startActivity(deepCopy)
346
+
347
+ return new Promise<ResolvedIntent<T>>((resolve, reject) => {
348
+ this.resultResolvers.set(deepCopy, resolve as (intent: ResolvedIntent<unknown>) => void)
349
+ this.startActivity(deepCopy).catch((e) => {
350
+ this.resultResolvers.delete(deepCopy)
351
+ reject(e)
352
+ })
353
+ })
318
354
  }
319
355
 
320
356
  isDestroyed(activity: Activity): boolean {
@@ -342,7 +378,7 @@ export class ActivityManager {
342
378
  // Clean up resources to prevent memory leaks
343
379
  this.cleanupActivity(stackEntry.activity)
344
380
 
345
- await this.manageActivityResult(stackEntry.activity, stackEntry.intent)
381
+ this.resolveActivityResult(stackEntry.activity, stackEntry.intent)
346
382
 
347
383
  // Resume the previous activity
348
384
  await this.bringLastActivityToFront()
@@ -353,6 +389,9 @@ export class ActivityManager {
353
389
  }
354
390
  await activity.onDestroy()
355
391
  activity.lifecycle = ActivityLifecycle.Destroyed
392
+
393
+ this.resolveActivityResult(activity, { action: '' })
394
+
356
395
  // This can happen when an activity finishes in one of the callback methods,
357
396
  // before it is added to the stack. In that case, we bring the last activity back to the front.
358
397
  await this.bringLastActivityToFront()
@@ -360,25 +399,21 @@ export class ActivityManager {
360
399
  this.#parent.requestUpdate()
361
400
  }
362
401
 
363
- private async manageActivityResult(activity: Activity, intent: Intent): Promise<void> {
364
- const { flags = 0, requestCode = -1 } = intent
365
- if (flags & IntentFlags.ForResult) {
366
- const all = [...this.activityStack, ...this.modalActivityStack]
367
- const target = all.find((entry) => entry.activity.hasRequestCode(requestCode))
368
- if (target) {
369
- if (target.activity.lifecycle === ActivityLifecycle.Destroyed) {
370
- return
371
- }
372
- if (target.activity.lifecycle === ActivityLifecycle.Resumed) {
373
- await target.activity.onPause()
374
- target.activity.lifecycle = ActivityLifecycle.Paused
375
- }
376
- // Let's create a shallow copy of the intent. We can disregard the ByReference flag here,
377
- // as we override the data with the result data.
378
- const intentCopy = { ...intent }
379
- intentCopy.data = activity.getResult()
380
- await target.activity.onActivityResult(requestCode, activity.resultCode, intentCopy)
402
+ private resolveActivityResult(activity: Activity, originalIntent: Intent): void {
403
+ const resolver = this.resultResolvers.get(originalIntent)
404
+ // The activity might have not been started with startActivityForResult.
405
+ // A thing to consider for the future is whether we should throw an error in a case where
406
+ // the activity was started for result but the resolver was not found.
407
+ if (resolver) {
408
+ this.resultResolvers.delete(originalIntent)
409
+ // We construct the result intent.
410
+ // Ideally we would like to preserve the original intent structure but with new data.
411
+ const resultIntent: ResolvedIntent = {
412
+ ...originalIntent,
413
+ data: activity.getResult(),
414
+ result: activity.resultCode,
381
415
  }
416
+ resolver(resultIntent)
382
417
  }
383
418
  }
384
419
 
@@ -3,16 +3,12 @@ import { Activity } from './Activity.js'
3
3
  import { FragmentState, type FragmentOptions, FragmentManager } from './FragmentManager.js'
4
4
  import type { Application, UpdateRequest } from './Application.js'
5
5
  import { FragmentRenderer } from './renderer/FragmentRenderer.js'
6
- import { Intent, IntentResult } from './ActivityManager.js'
6
+ import { Intent, ResolvedIntent } from './ActivityManager.js'
7
7
  import { bound } from '../decorators/bound.js'
8
8
  import type { ActivityDetail, ActivityWithResultDetail } from '../events/IntentEvents.js'
9
9
  import { EventTypes } from '../events/EventTypes.js'
10
10
  import { type RefOrCallback } from 'lit/directives/ref.js'
11
11
 
12
- export interface PendingActivityResult {
13
- onResult(result: IntentResult, intent: Intent): void
14
- }
15
-
16
12
  /**
17
13
  * Similar to Activity, with lifecycle methods (onCreate, onAttach, onDetach, etc.).
18
14
  * The crucial difference is that a Fragment is always hosted by an `Activity` or
@@ -31,20 +27,6 @@ export class Fragment extends EventTarget {
31
27
  protected children = new Map<string, Fragment>()
32
28
  protected fragmentManager: FragmentManager
33
29
 
34
- /**
35
- * The request code used to start an activity for a result.
36
- */
37
- requestCode = -1
38
-
39
- /**
40
- * A list of pending activity results that were requested by the components
41
- * hosted in this fragment.
42
- * The key is the request code and the value contains the callback
43
- * that will be called when the activity result is received.
44
- * The callback is called with the result code and the resulting intent.
45
- */
46
- pendingActivityResult: Map<number, PendingActivityResult> = new Map<number, PendingActivityResult>()
47
-
48
30
  #renderer: FragmentRenderer
49
31
 
50
32
  get renderer(): FragmentRenderer {
@@ -230,7 +212,7 @@ export class Fragment extends EventTarget {
230
212
  *
231
213
  * @example
232
214
  * // In your parent fragment's render method:
233
- * html`<div ${ref(this.createFragmentRef('my-child-fragment'))}></div>`
215
+ * // html`<div ${ref(this.createFragmentRef('my-child-fragment'))}></div>`
234
216
  */
235
217
  createFragmentRef(key: string): RefOrCallback<Element> {
236
218
  return (element?: Element) => {
@@ -268,14 +250,24 @@ export class Fragment extends EventTarget {
268
250
 
269
251
  /**
270
252
  * Starts an activity for result.
253
+ * @template T The type of the result data expected.
271
254
  * @param intent The intent to start.
255
+ * @returns A promise that resolves when the started activity finishes, returning the result Intent with
256
+ * data of type T.
257
+ * @example
258
+ * ```typescript
259
+ * const resultIntent = await this.startActivityForResult<{ userId: string }>({ action: 'pickUser' });
260
+ * if (resultIntent.resultCode === IntentResult.RESULT_OK) {
261
+ * console.log(resultIntent.data?.userId);
262
+ * }
263
+ * ```
272
264
  */
273
- async startActivityForResult(intent: Intent): Promise<void> {
265
+ async startActivityForResult<T>(intent: Intent): Promise<ResolvedIntent<T | undefined>> {
274
266
  const activity = this.getActivity()
275
267
  if (!activity) {
276
- throw new Error(`The fragment has no activity. Unable to start an intent.`)
268
+ throw new Error('Fragment is not attached to an activity')
277
269
  }
278
- this.requestCode = await activity.startActivityForResult(intent)
270
+ return activity.startActivityForResult<T>(intent)
279
271
  }
280
272
 
281
273
  /**
@@ -290,44 +282,6 @@ export class Fragment extends EventTarget {
290
282
  await activity.startActivity(intent)
291
283
  }
292
284
 
293
- /**
294
- * The callback method that is triggered when another activity that was
295
- * started for result finishes.
296
- *
297
- * @param requestCode The request code that was used to start the activity.
298
- * @param data The data that was passed back.
299
- * @param intent The intent that was used to start the activity.
300
- */
301
- async onActivityResult(requestCode: number, resultCode: IntentResult, intent: Intent): Promise<void> {
302
- const { pendingActivityResult, children } = this
303
- if (pendingActivityResult.has(requestCode)) {
304
- const { onResult } = pendingActivityResult.get(requestCode) as PendingActivityResult
305
- pendingActivityResult.delete(requestCode)
306
- onResult(resultCode, intent)
307
- return
308
- }
309
- for (const fragment of children.values()) {
310
- if (fragment.hasRequestCode(requestCode)) {
311
- fragment.onActivityResult(requestCode, resultCode, intent)
312
- return
313
- }
314
- }
315
- this.requestCode = -1
316
- if (this.constructor === Fragment) {
317
- // eslint-disable-next-line no-console
318
- console.info('Fragment#onActivityResult() not implemented', requestCode, resultCode, intent)
319
- }
320
- }
321
-
322
- hasRequestCode(requestCode: number): boolean {
323
- for (const fragment of this.children.values()) {
324
- if (fragment.hasRequestCode(requestCode)) {
325
- return true
326
- }
327
- }
328
- return this.requestCode === requestCode
329
- }
330
-
331
285
  /**
332
286
  * A handler for the intent event dispatched by web components hosted by this fragment.
333
287
  *
@@ -351,11 +305,8 @@ export class Fragment extends EventTarget {
351
305
  }
352
306
  if (event.type === EventTypes.Intent.startActivityForResult) {
353
307
  const info = event.detail as ActivityWithResultDetail
354
- const code = await activity.startActivityForResult(info.intent)
355
- this.requestCode = code
356
- this.pendingActivityResult.set(code, {
357
- onResult: info.onResult,
358
- })
308
+ const result = await this.startActivityForResult(info.intent)
309
+ info.onResult(result.result, result)
359
310
  } else if (event.type === EventTypes.Intent.startActivity) {
360
311
  await activity.startActivity(event.detail.intent)
361
312
  } else {
@@ -210,18 +210,4 @@ export class FragmentManager {
210
210
  }
211
211
  }
212
212
  }
213
-
214
- /**
215
- * Finds a fragment by activity request code.
216
- * @param requestCode The request code to find the fragment by.
217
- * @returns The corresponding fragment or `null`.
218
- */
219
- findByRequestCode(requestCode: number): Fragment | null {
220
- for (const [, fragment] of this.fragments) {
221
- if (fragment.hasRequestCode(requestCode)) {
222
- return fragment
223
- }
224
- }
225
- return null
226
- }
227
213
  }