@api-client/ui 0.6.5 → 0.6.7

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.
Files changed (30) hide show
  1. package/build/src/core/Activity.d.ts +7 -5
  2. package/build/src/core/Activity.d.ts.map +1 -1
  3. package/build/src/core/Activity.js +6 -4
  4. package/build/src/core/Activity.js.map +1 -1
  5. package/build/src/core/ActivityManager.d.ts +35 -2
  6. package/build/src/core/ActivityManager.d.ts.map +1 -1
  7. package/build/src/core/ActivityManager.js +11 -0
  8. package/build/src/core/ActivityManager.js.map +1 -1
  9. package/build/src/core/Fragment.d.ts +12 -2
  10. package/build/src/core/Fragment.d.ts.map +1 -1
  11. package/build/src/core/Fragment.js +11 -2
  12. package/build/src/core/Fragment.js.map +1 -1
  13. package/build/src/elements/setup/internals/OrganizationSelector.js +2 -2
  14. package/build/src/elements/setup/internals/OrganizationSelector.js.map +1 -1
  15. package/build/src/md/button/internals/base.d.ts +4 -1
  16. package/build/src/md/button/internals/base.d.ts.map +1 -1
  17. package/build/src/md/button/internals/base.js +4 -0
  18. package/build/src/md/button/internals/base.js.map +1 -1
  19. package/build/src/services/OrganizationService.d.ts +44 -6
  20. package/build/src/services/OrganizationService.d.ts.map +1 -1
  21. package/build/src/services/OrganizationService.js +79 -24
  22. package/build/src/services/OrganizationService.js.map +1 -1
  23. package/build/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +1 -1
  25. package/src/core/Activity.ts +8 -6
  26. package/src/core/ActivityManager.ts +41 -6
  27. package/src/core/Fragment.ts +14 -4
  28. package/src/elements/setup/internals/OrganizationSelector.ts +2 -2
  29. package/src/md/button/internals/base.ts +5 -1
  30. package/src/services/OrganizationService.ts +84 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
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'
@@ -301,17 +301,19 @@ export class Activity extends EventTarget {
301
301
 
302
302
  /**
303
303
  * Starts a new activity for a result.
304
+ * @template T The type of the result data expected.
304
305
  * @param intent The intent to start.
305
- * @returns A promise that resolves when the started activity finishes, returning the result Intent.
306
+ * @returns A promise that resolves when the started activity finishes, returning the result Intent with
307
+ * data of type T.
306
308
  * @example
307
309
  * ```typescript
308
- * const resultIntent = await this.startActivityForResult({ action: 'pickFile' });
310
+ * const resultIntent = await this.startActivityForResult<{ userId: string }>({ action: 'pickUser' });
309
311
  * if (resultIntent.resultCode === IntentResult.RESULT_OK) {
310
- * // handle result
312
+ * console.log(resultIntent.data?.userId);
311
313
  * }
312
314
  * ```
313
315
  */
314
- async startActivityForResult(intent: Intent): Promise<Intent> {
316
+ async startActivityForResult<T>(intent: Intent): Promise<ResolvedIntent<T | undefined>> {
315
317
  const { manager } = this.getApplication()
316
318
  return manager.startActivityForResult(intent)
317
319
  }
@@ -355,7 +357,7 @@ export class Activity extends EventTarget {
355
357
  if (event.type === EventTypes.Intent.startActivityForResult) {
356
358
  const info = event.detail as ActivityWithResultDetail
357
359
  const result = await this.startActivityForResult(info.intent)
358
- info.onResult(IntentResult.RESULT_OK, result)
360
+ info.onResult(result.result, result)
359
361
  } else if (event.type === EventTypes.Intent.startActivity) {
360
362
  await this.startActivity(event.detail.intent)
361
363
  } else {
@@ -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,
@@ -137,7 +161,7 @@ export class ActivityManager {
137
161
  /**
138
162
  * Stores the resolvers for activities started for result.
139
163
  */
140
- private resultResolvers = new Map<Intent, (intent: Intent) => void>()
164
+ private resultResolvers = new Map<Intent, (intent: ResolvedIntent) => void>()
141
165
 
142
166
  /**
143
167
  * Represents the activity stack. It is used to manage
@@ -291,9 +315,19 @@ export class ActivityManager {
291
315
  /**
292
316
  * Starts an activity that should return a result to the calling activity.
293
317
  *
318
+ * @template T The type of the result data expected.
294
319
  * @param intent The intent to start the activity.
295
- */
296
- async startActivityForResult(intent: Intent): Promise<Intent> {
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>> {
297
331
  const byReference = (intent.flags ?? 0) & IntentFlags.ByReference
298
332
  const shallowCopy = { ...intent }
299
333
  const data = shallowCopy.data
@@ -310,8 +344,8 @@ export class ActivityManager {
310
344
  deepCopy.flags = IntentFlags.ForResult
311
345
  }
312
346
 
313
- return new Promise<Intent>((resolve, reject) => {
314
- this.resultResolvers.set(deepCopy, resolve)
347
+ return new Promise<ResolvedIntent<T>>((resolve, reject) => {
348
+ this.resultResolvers.set(deepCopy, resolve as (intent: ResolvedIntent<unknown>) => void)
315
349
  this.startActivity(deepCopy).catch((e) => {
316
350
  this.resultResolvers.delete(deepCopy)
317
351
  reject(e)
@@ -374,9 +408,10 @@ export class ActivityManager {
374
408
  this.resultResolvers.delete(originalIntent)
375
409
  // We construct the result intent.
376
410
  // Ideally we would like to preserve the original intent structure but with new data.
377
- const resultIntent: Intent = {
411
+ const resultIntent: ResolvedIntent = {
378
412
  ...originalIntent,
379
413
  data: activity.getResult(),
414
+ result: activity.resultCode,
380
415
  }
381
416
  resolver(resultIntent)
382
417
  }
@@ -3,7 +3,7 @@ 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'
@@ -250,14 +250,24 @@ export class Fragment extends EventTarget {
250
250
 
251
251
  /**
252
252
  * Starts an activity for result.
253
+ * @template T The type of the result data expected.
253
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
+ * ```
254
264
  */
255
- async startActivityForResult(intent: Intent): Promise<Intent> {
265
+ async startActivityForResult<T>(intent: Intent): Promise<ResolvedIntent<T | undefined>> {
256
266
  const activity = this.getActivity()
257
267
  if (!activity) {
258
268
  throw new Error('Fragment is not attached to an activity')
259
269
  }
260
- return activity.startActivityForResult(intent)
270
+ return activity.startActivityForResult<T>(intent)
261
271
  }
262
272
 
263
273
  /**
@@ -296,7 +306,7 @@ export class Fragment extends EventTarget {
296
306
  if (event.type === EventTypes.Intent.startActivityForResult) {
297
307
  const info = event.detail as ActivityWithResultDetail
298
308
  const result = await this.startActivityForResult(info.intent)
299
- info.onResult(IntentResult.RESULT_OK, result)
309
+ info.onResult(result.result, result)
300
310
  } else if (event.type === EventTypes.Intent.startActivity) {
301
311
  await activity.startActivity(event.detail.intent)
302
312
  } else {
@@ -85,7 +85,7 @@ export default class OrganizationSelector extends LitElement {
85
85
  }
86
86
 
87
87
  protected override render(): unknown {
88
- return html`<span class="container">${this.renderTrigger()} ${this.renderMenu()}</span>`
88
+ return html`${this.renderTrigger()} ${this.renderMenu()}`
89
89
  }
90
90
 
91
91
  protected renderTrigger(): TemplateResult {
@@ -97,7 +97,7 @@ export default class OrganizationSelector extends LitElement {
97
97
  aria-label="Current organization: ${orgName}"
98
98
  trailingicon
99
99
  color="tonal"
100
- popoverTarget="org-selector-menu"
100
+ popovertarget="org-selector-menu"
101
101
  >
102
102
  ${orgName}
103
103
  <ui-icon slot="icon">arrow_drop_down</ui-icon>
@@ -131,6 +131,9 @@ export default class BaseButton extends UiElement {
131
131
  /**
132
132
  * Turns a `<ui-button>` element into a popover control button; takes the ID
133
133
  * of the popover element to control as its value.
134
+ *
135
+ * Note: this is required for now as the spec only allows to control popovers
136
+ * via buttons. Custom elements are not allowed to control popovers.
134
137
  * @attribute
135
138
  */
136
139
  @property({ type: String, reflect: true }) accessor popoverTarget: string | undefined
@@ -146,7 +149,7 @@ export default class BaseButton extends UiElement {
146
149
  * it will be shown; if the popover is showing, it will be hidden. If popoverTargetAction is omitted,
147
150
  * "toggle" is the default action that will be performed by the control button.
148
151
  */
149
- @property({ type: String, reflect: true }) accessor popoverTargetAction: string | undefined
152
+ @property({ type: String, reflect: true }) accessor popoverTargetAction: 'hide' | 'show' | 'toggle' | undefined
150
153
  /**
151
154
  * When true, the focus ring effect will be constrained to the inside of the button's bounds.
152
155
  * @attribute
@@ -312,6 +315,7 @@ export default class BaseButton extends UiElement {
312
315
  } else if (action === 'show') {
313
316
  element.showPopover()
314
317
  } else {
318
+ // default to toggle
315
319
  element.togglePopover()
316
320
  }
317
321
  element.focus()
@@ -56,9 +56,74 @@ export default class OrganizationService extends Service {
56
56
  protected override async onCreate(): Promise<void> {
57
57
  const url = new URL(window.location.href)
58
58
  this.pendingOid = url.searchParams.get('oid')
59
+ this.revalidate()
59
60
  }
60
61
 
61
- async getUserOrganization(id: string): Promise<IOrganization> {
62
+ /**
63
+ * @deprecated Use #setOrganization instead
64
+ */
65
+ setupUserOrganization(org: IOrganization): Promise<void> {
66
+ return this.setOrganization(org)
67
+ }
68
+
69
+ /**
70
+ * Sets an org as the current user org.
71
+ * @param org The organization to add.
72
+ */
73
+ async setOrganization(org: IOrganization): Promise<void> {
74
+ const hasOrg = this.organizations.some((i) => i.key === org.key)
75
+ if (!hasOrg) {
76
+ this.organizations.push(org)
77
+ }
78
+ const config = await services.get('config')
79
+ await config.local.set(CurrentOrganizationKey, org.key)
80
+ this.organizationId = org.key
81
+ }
82
+
83
+ /**
84
+ * Adds an organization to the list of organizations.
85
+ * It also updated the cache. Used when creating a new org.
86
+ * @param org The organization to add.
87
+ */
88
+ async addOrganization(org: IOrganization): Promise<void> {
89
+ const hasOrg = this.organizations.some((i) => i.key === org.key)
90
+ if (!hasOrg) {
91
+ this.organizations.push(org)
92
+ await this.#saveCache()
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Removes an organization from the list of organizations.
98
+ * It also updated the cache. Used when deleting an org.
99
+ * @param org The organization to remove.
100
+ */
101
+ async removeOrganization(org: IOrganization): Promise<void> {
102
+ const idx = this.organizations.findIndex((i) => i.key === org.key)
103
+ if (idx !== -1) {
104
+ this.organizations.splice(idx, 1)
105
+ await this.#saveCache()
106
+ if (this.organizationId === org.key) {
107
+ this.organizationId = undefined
108
+ const config = await services.get('config')
109
+ await config.local.delete(CurrentOrganizationKey)
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @deprecated Use #readOrganization instead
116
+ */
117
+ getUserOrganization(id: string): Promise<IOrganization> {
118
+ return this.readOrganization(id)
119
+ }
120
+
121
+ /**
122
+ * Reads an organization from the list of organizations.
123
+ * @param id The organization ID.
124
+ * @returns The organization.
125
+ */
126
+ async readOrganization(id: string): Promise<IOrganization> {
62
127
  await this.#updateComplete
63
128
  const org = this.organizations.find((i) => i.key === id)
64
129
  if (!org) {
@@ -67,6 +132,10 @@ export default class OrganizationService extends Service {
67
132
  return org
68
133
  }
69
134
 
135
+ /**
136
+ * Gets the current user organization.
137
+ * @returns The current organization or undefined if not set.
138
+ */
70
139
  async currentOrganization(): Promise<IOrganization | undefined> {
71
140
  if (this.state === State.Checking) {
72
141
  await this.#updateComplete
@@ -74,11 +143,14 @@ export default class OrganizationService extends Service {
74
143
  if (!this.organizationId) {
75
144
  return undefined
76
145
  }
77
- // TODO: we will store the orgs list in the local/session store
78
- // so we won't be pinging server with every page reload.
79
146
  return this.organizations.find((i) => i.key === this.organizationId)
80
147
  }
81
148
 
149
+ /**
150
+ * Checks if the user has a specific grant type in the current organization.
151
+ * @param grantType The grant type to check.
152
+ * @returns `true` if the user has the specified grant type, `false` otherwise.
153
+ */
82
154
  protected async hasGrantType(grantType: UserOrganizationGrantType): Promise<boolean> {
83
155
  const org = await this.currentOrganization()
84
156
  if (!org) {
@@ -87,6 +159,11 @@ export default class OrganizationService extends Service {
87
159
  return org.grantType === grantType
88
160
  }
89
161
 
162
+ /**
163
+ * Checks if the user has any of the specified grant types in the current organization.
164
+ * @param types The grant types to check.
165
+ * @returns `true` if the user has any of the specified grant types, `false` otherwise.
166
+ */
90
167
  async hasRoles(types: UserOrganizationGrantType[]): Promise<boolean> {
91
168
  const org = await this.currentOrganization()
92
169
  if (!org) {
@@ -135,9 +212,6 @@ export default class OrganizationService extends Service {
135
212
  if (this.state === State.Checking) {
136
213
  return this.#updateComplete as Promise<CompletionState>
137
214
  }
138
- if (this.organizationId) {
139
- return Promise.resolve(CompletionState.OK)
140
- }
141
215
  this.state = State.Checking
142
216
  this.#updateComplete = new Promise<CompletionState>((resolver) => {
143
217
  this.#updateResolver = resolver
@@ -175,12 +249,12 @@ export default class OrganizationService extends Service {
175
249
  return false
176
250
  }
177
251
 
178
- async #saveCache(orgs: IOrganization[]): Promise<void> {
252
+ async #saveCache(): Promise<void> {
179
253
  const cache = await services.get('cache')
180
- if (orgs.length === 0) {
254
+ if (this.organizations.length === 0) {
181
255
  await cache.delete(OrgsCacheKey)
182
256
  } else {
183
- await cache.set<IOrganization[]>(OrgsCacheKey, orgs, cache.TTL.OneHour)
257
+ await cache.set<IOrganization[]>(OrgsCacheKey, this.organizations, cache.TTL.OneHour)
184
258
  }
185
259
  }
186
260
 
@@ -188,7 +262,7 @@ export default class OrganizationService extends Service {
188
262
  const sdk = await services.get('sdk')
189
263
  const orgs = await sdk.organizations.list()
190
264
  this.organizations = orgs.items || []
191
- await this.#saveCache(orgs.items)
265
+ await this.#saveCache()
192
266
  }
193
267
 
194
268
  #resolve(state: CompletionState): void {
@@ -220,19 +294,4 @@ export default class OrganizationService extends Service {
220
294
  }
221
295
  return result
222
296
  }
223
-
224
- /**
225
- * When the user has no organizations, it set the organization as the only one
226
- * and persists the organization id as the selected organization.
227
- * @param org The organization to add.
228
- */
229
- async setupUserOrganization(org: IOrganization): Promise<void> {
230
- const hasOrg = this.organizations.some((i) => i.key === org.key)
231
- if (!hasOrg) {
232
- this.organizations.push(org)
233
- }
234
- const config = await services.get('config')
235
- await config.local.set(CurrentOrganizationKey, org.key)
236
- this.organizationId = org.key
237
- }
238
297
  }