@api-client/ui 0.6.4 → 0.6.5

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.5",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.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,26 @@ 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.
291
304
  * @param intent The intent to start.
292
- * @returns The request code for the started activity.
305
+ * @returns A promise that resolves when the started activity finishes, returning the result Intent.
293
306
  * @example
294
307
  * ```typescript
295
- * const code = await this.startActivityForResult({ action: 'pickFile' });
308
+ * const resultIntent = await this.startActivityForResult({ action: 'pickFile' });
309
+ * if (resultIntent.resultCode === IntentResult.RESULT_OK) {
310
+ * // handle result
311
+ * }
296
312
  * ```
297
313
  */
298
- async startActivityForResult(intent: Intent): Promise<number> {
314
+ async startActivityForResult(intent: Intent): Promise<Intent> {
299
315
  const { manager } = this.getApplication()
300
- const code = manager.createRequestCode()
301
- this.pendingRequestCodes.push(code)
302
- await manager.startActivityForResult(intent, code)
303
- return code
316
+ return manager.startActivityForResult(intent)
304
317
  }
305
318
 
306
319
  /**
@@ -317,57 +330,6 @@ export class Activity extends EventTarget {
317
330
  this.resultData = data
318
331
  }
319
332
 
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
333
  /**
372
334
  * Gets the result data set by `setResult`.
373
335
  * @returns The result data or `undefined`.
@@ -392,7 +354,8 @@ export class Activity extends EventTarget {
392
354
  async handleIntentEvent(event: CustomEvent<ActivityDetail | ActivityWithResultDetail>): Promise<void> {
393
355
  if (event.type === EventTypes.Intent.startActivityForResult) {
394
356
  const info = event.detail as ActivityWithResultDetail
395
- await this.startActivityForResult(info.intent)
357
+ const result = await this.startActivityForResult(info.intent)
358
+ info.onResult(IntentResult.RESULT_OK, result)
396
359
  } else if (event.type === EventTypes.Intent.startActivity) {
397
360
  await this.startActivity(event.detail.intent)
398
361
  } 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
  /**
@@ -134,6 +134,11 @@ export class ActivityManager {
134
134
  */
135
135
  private activityClasses = new Map<string, typeof Activity>()
136
136
 
137
+ /**
138
+ * Stores the resolvers for activities started for result.
139
+ */
140
+ private resultResolvers = new Map<Intent, (intent: Intent) => void>()
141
+
137
142
  /**
138
143
  * Represents the activity stack. It is used to manage
139
144
  * the UI state after calling the back action.
@@ -184,15 +189,6 @@ export class ActivityManager {
184
189
  this.activityClasses.set(action, activityClass as typeof Activity)
185
190
  }
186
191
 
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
192
  /**
197
193
  * Creates an activity and pushes it into the stack, but it does not initialize any of the lifecycle methods.
198
194
  * It is a way to put an activity at some place of the stack,
@@ -210,21 +206,21 @@ export class ActivityManager {
210
206
  this.activityStack.push(stackEntry)
211
207
  }
212
208
 
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
- }
209
+ // setupRoute(intent: Intent): void {
210
+ // if (intent.data) {
211
+ // const typed = intent.data as { uri?: string }
212
+ // if (typed.uri) {
213
+ // navigateScreen(typed.uri)
214
+ // }
215
+ // }
216
+ // }
221
217
 
222
218
  /**
223
219
  * Starts a new activity and brings it to the foreground.
224
220
  *
225
221
  * @param intent The intent that created this activity.
226
222
  */
227
- async startActivity(intent: Intent): Promise<void> {
223
+ async startActivity(intent: Intent): Promise<Activity> {
228
224
  const { currentActivity, activityStack } = this
229
225
  const lastIndex = activityStack.findLastIndex((entry) => entry.action === intent.action)
230
226
  // if the activity is in the history list, we bring it up and update intent.
@@ -239,9 +235,9 @@ export class ActivityManager {
239
235
  })
240
236
  await info.activity.onNewIntent(intent)
241
237
  // TODO: Check if the activity is destroyed.
242
- this.setupRoute(intent)
238
+ // this.setupRoute(intent)
243
239
  await this.updateCurrentActivity(info.activity, false)
244
- return
240
+ return info.activity
245
241
  }
246
242
  const activity = this.buildActivity(intent)
247
243
  if (currentActivity) {
@@ -263,16 +259,17 @@ export class ActivityManager {
263
259
  await activity.onCreate(intent)
264
260
  if (this.isDestroyed(activity)) {
265
261
  // the activity finished and the manager already took care of this situation.
266
- return
262
+ return activity
267
263
  }
268
264
  activity.lifecycle = ActivityLifecycle.Created
269
265
 
270
266
  await this.updateCurrentActivity(activity, !!isModal)
271
267
  if (this.isDestroyed(activity)) {
272
268
  // the activity finished and the manager already took care of this situation.
273
- return
269
+ return activity
274
270
  }
275
- this.setupRoute(intent)
271
+ // this.setupRoute(intent)
272
+ return activity
276
273
  }
277
274
 
278
275
  private buildActivity(intent: Intent): Activity {
@@ -293,12 +290,10 @@ export class ActivityManager {
293
290
 
294
291
  /**
295
292
  * Starts an activity that should return a result to the calling activity.
296
- * The result should be a unique number across the application.
293
+ *
297
294
  * @param intent The intent to start the activity.
298
- * @param requestCode The request code used to match the activity result.
299
295
  */
300
- async startActivityForResult(intent: Intent, requestCode: number): Promise<void> {
301
- intent.requestCode = requestCode
296
+ async startActivityForResult(intent: Intent): Promise<Intent> {
302
297
  const byReference = (intent.flags ?? 0) & IntentFlags.ByReference
303
298
  const shallowCopy = { ...intent }
304
299
  const data = shallowCopy.data
@@ -314,7 +309,14 @@ export class ActivityManager {
314
309
  } else {
315
310
  deepCopy.flags = IntentFlags.ForResult
316
311
  }
317
- await this.startActivity(deepCopy)
312
+
313
+ return new Promise<Intent>((resolve, reject) => {
314
+ this.resultResolvers.set(deepCopy, resolve)
315
+ this.startActivity(deepCopy).catch((e) => {
316
+ this.resultResolvers.delete(deepCopy)
317
+ reject(e)
318
+ })
319
+ })
318
320
  }
319
321
 
320
322
  isDestroyed(activity: Activity): boolean {
@@ -342,7 +344,7 @@ export class ActivityManager {
342
344
  // Clean up resources to prevent memory leaks
343
345
  this.cleanupActivity(stackEntry.activity)
344
346
 
345
- await this.manageActivityResult(stackEntry.activity, stackEntry.intent)
347
+ this.resolveActivityResult(stackEntry.activity, stackEntry.intent)
346
348
 
347
349
  // Resume the previous activity
348
350
  await this.bringLastActivityToFront()
@@ -353,6 +355,9 @@ export class ActivityManager {
353
355
  }
354
356
  await activity.onDestroy()
355
357
  activity.lifecycle = ActivityLifecycle.Destroyed
358
+
359
+ this.resolveActivityResult(activity, { action: '' })
360
+
356
361
  // This can happen when an activity finishes in one of the callback methods,
357
362
  // before it is added to the stack. In that case, we bring the last activity back to the front.
358
363
  await this.bringLastActivityToFront()
@@ -360,25 +365,20 @@ export class ActivityManager {
360
365
  this.#parent.requestUpdate()
361
366
  }
362
367
 
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)
368
+ private resolveActivityResult(activity: Activity, originalIntent: Intent): void {
369
+ const resolver = this.resultResolvers.get(originalIntent)
370
+ // The activity might have not been started with startActivityForResult.
371
+ // A thing to consider for the future is whether we should throw an error in a case where
372
+ // the activity was started for result but the resolver was not found.
373
+ if (resolver) {
374
+ this.resultResolvers.delete(originalIntent)
375
+ // We construct the result intent.
376
+ // Ideally we would like to preserve the original intent structure but with new data.
377
+ const resultIntent: Intent = {
378
+ ...originalIntent,
379
+ data: activity.getResult(),
381
380
  }
381
+ resolver(resultIntent)
382
382
  }
383
383
  }
384
384
 
@@ -9,10 +9,6 @@ import type { ActivityDetail, ActivityWithResultDetail } from '../events/IntentE
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) => {
@@ -270,12 +252,12 @@ export class Fragment extends EventTarget {
270
252
  * Starts an activity for result.
271
253
  * @param intent The intent to start.
272
254
  */
273
- async startActivityForResult(intent: Intent): Promise<void> {
255
+ async startActivityForResult(intent: Intent): Promise<Intent> {
274
256
  const activity = this.getActivity()
275
257
  if (!activity) {
276
- throw new Error(`The fragment has no activity. Unable to start an intent.`)
258
+ throw new Error('Fragment is not attached to an activity')
277
259
  }
278
- this.requestCode = await activity.startActivityForResult(intent)
260
+ return activity.startActivityForResult(intent)
279
261
  }
280
262
 
281
263
  /**
@@ -290,44 +272,6 @@ export class Fragment extends EventTarget {
290
272
  await activity.startActivity(intent)
291
273
  }
292
274
 
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
275
  /**
332
276
  * A handler for the intent event dispatched by web components hosted by this fragment.
333
277
  *
@@ -351,11 +295,8 @@ export class Fragment extends EventTarget {
351
295
  }
352
296
  if (event.type === EventTypes.Intent.startActivityForResult) {
353
297
  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
- })
298
+ const result = await this.startActivityForResult(info.intent)
299
+ info.onResult(IntentResult.RESULT_OK, result)
359
300
  } else if (event.type === EventTypes.Intent.startActivity) {
360
301
  await activity.startActivity(event.detail.intent)
361
302
  } 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
  }