@lotics/app-sdk 0.78.0 → 0.79.0
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/dist/src/members.d.ts +59 -4
- package/dist/src/members.js +15 -0
- package/docs/members_and_options.md +23 -8
- package/package.json +1 -1
package/dist/src/members.d.ts
CHANGED
|
@@ -18,10 +18,24 @@
|
|
|
18
18
|
* not tell which. That is why no register row ever rendered an avatar — the
|
|
19
19
|
* data was absent and nothing said so.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* It happened a second time, and the fix is the same: the roster returned
|
|
22
|
+
* neither `groups` nor `role` long after a resolved cell carried both, so an
|
|
23
|
+
* app reading `m.groups` off `useMembers()` got `undefined` and drew nothing.
|
|
24
|
+
* Read a field here and you may read it on either door.
|
|
25
|
+
*
|
|
26
|
+
* The private fields share ONE boundary, not four: an authenticated member of
|
|
27
|
+
* the app's own org sees `email`, `image`, `groups` and `role`; an anonymous
|
|
28
|
+
* visitor to a public app sees `id` and `name` (plus `archived`, which is not
|
|
29
|
+
* private — see the field). Absent ≠ empty — `image: null` means the member has
|
|
30
|
+
* no photo, `image` MISSING means you were never told, and `groups: []` means
|
|
31
|
+
* they are on no team while a missing `groups` means the same "not told". Never
|
|
32
|
+
* collapse the two: one is a fact about a colleague, the other is a fact about
|
|
33
|
+
* your own permissions.
|
|
34
|
+
*
|
|
35
|
+
* The one field the doors legitimately differ on is `archived`, and it is a
|
|
36
|
+
* difference of ROW SET rather than of shape: a resolved cell must keep naming
|
|
37
|
+
* whoever handled a record two years ago, while the roster answers "who may I
|
|
38
|
+
* assign?" and never offers a departed member at all.
|
|
25
39
|
*/
|
|
26
40
|
export interface ResolvedMember {
|
|
27
41
|
id: string;
|
|
@@ -40,6 +54,47 @@ export interface ResolvedMember {
|
|
|
40
54
|
* department field; a member group is what an org uses to say Sale, Kế toán,
|
|
41
55
|
* CSKH. `[]` when the member is in none. Omitted on public-app responses. */
|
|
42
56
|
groups?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* The member's ORGANIZATION role. Omitted on public-app responses, and also
|
|
59
|
+
* when the id did not resolve — there is no member to have a level.
|
|
60
|
+
*
|
|
61
|
+
* It arrives RAW, and it is your job to translate it: the platform ships no
|
|
62
|
+
* display word for it, because a server that picked one would leak English
|
|
63
|
+
* into every localized app. Map it yourself (`admin` → "Quản trị viên") next
|
|
64
|
+
* to the rest of your vocabulary.
|
|
65
|
+
*
|
|
66
|
+
* It is a PERMISSION level and not a job title. Everyone who reads "Admin"
|
|
67
|
+
* beside a name on a sales register will read it as rank; if the question
|
|
68
|
+
* your screen answers is "who is this person in the company", the answer is
|
|
69
|
+
* `groups`, not this.
|
|
70
|
+
*/
|
|
71
|
+
role?: "owner" | "admin" | "member";
|
|
72
|
+
/**
|
|
73
|
+
* ISO timestamp of when this person joined the organization. Omitted on
|
|
74
|
+
* public-app responses, and when the id did not resolve — there is no
|
|
75
|
+
* membership to have begun.
|
|
76
|
+
*
|
|
77
|
+
* Render it at whatever precision your question needs; `@lotics/ui`'s
|
|
78
|
+
* `MemberProfileCard` shows month and year, because "is this the new person?"
|
|
79
|
+
* does not want a day.
|
|
80
|
+
*/
|
|
81
|
+
joined?: string;
|
|
82
|
+
/**
|
|
83
|
+
* `true` when this person has LEFT the organization — omitted otherwise,
|
|
84
|
+
* never `false`, because it rides every cell of every row and current staff
|
|
85
|
+
* are the overwhelming case.
|
|
86
|
+
*
|
|
87
|
+
* It arrives on both audiences: a departed colleague reading as a current
|
|
88
|
+
* assignee is wrong on a public app too, and it discloses less than the name
|
|
89
|
+
* already beside it. Feed it to `inactive` on `MemberChip` /
|
|
90
|
+
* `MemberProfileCard` so a record that still names them reads as history
|
|
91
|
+
* rather than as a live assignment.
|
|
92
|
+
*
|
|
93
|
+
* You will not see it on the `useMembers` roster, and that is correct rather
|
|
94
|
+
* than missing: the roster answers "who may I ASSIGN?" and departed members
|
|
95
|
+
* are not candidates, so it never returns one.
|
|
96
|
+
*/
|
|
97
|
+
archived?: true;
|
|
43
98
|
}
|
|
44
99
|
/**
|
|
45
100
|
* Parse a `useQuery` cell value into `ResolvedMember[]`. Returns `[]` for
|
package/dist/src/members.js
CHANGED
|
@@ -41,6 +41,21 @@ export function readMembers(value) {
|
|
|
41
41
|
? obj.groups.filter((g) => typeof g === "string")
|
|
42
42
|
: [];
|
|
43
43
|
}
|
|
44
|
+
// A CLOSED enum, so an unrecognized value is dropped rather than passed
|
|
45
|
+
// through: the field's whole worth is that a caller can switch on it, and a
|
|
46
|
+
// server that grew a fourth role must not have apps rendering the raw word
|
|
47
|
+
// in a UI that has no translation for it.
|
|
48
|
+
if (obj.role === "owner" || obj.role === "admin" || obj.role === "member")
|
|
49
|
+
m.role = obj.role;
|
|
50
|
+
// A non-empty STRING or nothing: an empty date is not a date, and letting
|
|
51
|
+
// "" through would render an empty labelled row rather than no row.
|
|
52
|
+
if (typeof obj.joined === "string" && obj.joined !== "")
|
|
53
|
+
m.joined = obj.joined;
|
|
54
|
+
// The server sends this ONLY for a departed member and only as `true`, so
|
|
55
|
+
// the truthy test is the whole contract — there is no `false` to carry, and
|
|
56
|
+
// inventing one would put the key on every current member's cell.
|
|
57
|
+
if (obj.archived === true)
|
|
58
|
+
m.archived = true;
|
|
44
59
|
out.push(m);
|
|
45
60
|
}
|
|
46
61
|
return out;
|
|
@@ -108,13 +108,13 @@ cell's own `{ key, label }`, which renders as a neutral badge. Never hand-map op
|
|
|
108
108
|
## Member cells (`readMembers`)
|
|
109
109
|
|
|
110
110
|
A `select_member` column's storage shape is a bare array of member ids. The server rewrites every
|
|
111
|
-
projected `select_member` cell to `Array<{ id, name, email?, image?, groups? }>` —
|
|
112
|
-
`useMembers` returns**, so a member is a member wherever you got them from:
|
|
111
|
+
projected `select_member` cell to `Array<{ id, name, email?, image?, groups?, role?, joined?, archived? }>` —
|
|
112
|
+
**the same shape `useMembers` returns**, so a member is a member wherever you got them from:
|
|
113
113
|
|
|
114
|
-
- **`email`, `image` and `
|
|
115
|
-
members of the app's own organization. Anonymous visitors to a public app — and
|
|
116
|
-
orgs viewing a publicly shared app — get `{ id, name }` only. A face
|
|
117
|
-
exactly as an address is.
|
|
114
|
+
- **`email`, `image`, `groups`, `role` and `joined` share ONE gate.** All five are present only for
|
|
115
|
+
authenticated members of the app's own organization. Anonymous visitors to a public app — and
|
|
116
|
+
members of *other* orgs viewing a publicly shared app — get `{ id, name }` only. A face, a team
|
|
117
|
+
and a permission level are org-internal exactly as an address is.
|
|
118
118
|
- **Absent is not null.** A missing `image` key means you were not told; `image: null` means the
|
|
119
119
|
member genuinely has no photo. Same for `groups`: absent vs `[]`. Rendering the two the same way
|
|
120
120
|
turns a permission boundary into a missing-data bug.
|
|
@@ -122,6 +122,18 @@ projected `select_member` cell to `Array<{ id, name, email?, image?, groups? }>`
|
|
|
122
122
|
cookie-authenticated proxy, so an unsigned URL would not load inside an app iframe at all.
|
|
123
123
|
- **`groups` is the platform's "department".** There is no department field; a member group is what
|
|
124
124
|
an org uses to say Sale, Kế toán, CSKH. `[]` when the member is in none.
|
|
125
|
+
- **`role` arrives RAW (`owner` / `admin` / `member`) and you translate it.** The platform ships no
|
|
126
|
+
display word, because a server that picked one would leak English into every localized app — map
|
|
127
|
+
it next to the rest of your vocabulary. It is a PERMISSION level, not a job title: everyone reads
|
|
128
|
+
"Admin" beside a name as rank. If the question your screen asks is "who is this person in the
|
|
129
|
+
company", the answer is `groups`.
|
|
130
|
+
- **`joined` is an ISO timestamp of when the membership began.** Render it at whatever precision
|
|
131
|
+
your question needs — `@lotics/ui`'s `MemberProfileCard` shows month and year, because "is this
|
|
132
|
+
the new person?" does not want a day. Absent on a public response, and on an id that did not
|
|
133
|
+
resolve: there is no membership to have begun.
|
|
134
|
+
- **`archived: true` marks someone who has LEFT** — omitted otherwise, never `false`. It is the one
|
|
135
|
+
field that is NOT gated (a departed colleague reading as a current assignee is wrong on a public
|
|
136
|
+
app too). Feed it to `inactive` on `MemberChip` / `MemberProfileCard`.
|
|
125
137
|
- **An id that no longer resolves** (removed member, id outside the org) comes back with
|
|
126
138
|
`name: null` — an explicit missing state, never an empty string. Render a placeholder.
|
|
127
139
|
- Only ids already present in the projected rows are resolved — a member cell never exposes the
|
|
@@ -139,7 +151,10 @@ const { members, loading, error } = useMembers({ group: "grp_fulfillment" });
|
|
|
139
151
|
<MemberSelect members={members} value={assignee} onValueChange={setAssignee} />
|
|
140
152
|
```
|
|
141
153
|
|
|
142
|
-
Each member is the same `ResolvedMember` a cell carries — `{ id, name, email, image, groups }`.
|
|
154
|
+
Each member is the same `ResolvedMember` a cell carries — `{ id, name, email, image, groups, role, joined }`.
|
|
155
|
+
The one field it never carries is `archived`, and that is the answer rather than a gap: this roster
|
|
156
|
+
answers "who may I ASSIGN?", and a departed member is not a candidate, so it never returns one. A
|
|
157
|
+
resolved CELL is the display door and keeps naming them.
|
|
143
158
|
`image` is the avatar URL (a presigned URL valid 24 hours, or the member's external OAuth photo) and
|
|
144
159
|
may be `null`. `name` may be null/empty for members without a display name — fall back to `email`. On failure the hook does not throw: it
|
|
145
160
|
resolves `{ members: [], loading: false, error }`.
|
|
@@ -311,7 +326,7 @@ The SDK never imports `@lotics/ui` — the app owns the (thin) data→UI adapter
|
|
|
311
326
|
| Surface | Embedded (signed-in member) | Standalone / public (anonymous) |
|
|
312
327
|
| --- | --- | --- |
|
|
313
328
|
| `readSelect` cell enrichment | ✓ | ✓ |
|
|
314
|
-
| `readMembers` cell enrichment | ✓ (email + image + groups, same-org) | ✓ name-only |
|
|
329
|
+
| `readMembers` cell enrichment | ✓ (email + image + groups + role + joined, same-org) | ✓ name-only (+ `archived`) |
|
|
315
330
|
| `useFieldOptions` | ✓ | ✓ |
|
|
316
331
|
| `useMembers` | ✓ (same-org + declaration gates) | ✗ errors |
|
|
317
332
|
| `useViewer` | member id (view-as target) | `null` |
|