mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-10 22:01:40 +00:00
docs: add the easy teams recipe and playground examples
Covers the read-only surface an app actually has: list(), listMembers() and listDirectory(). Adds the teams recipe tag the build validates against, and a Teams section to the playground. Each example handles the two states that are easy to confuse: teams turned off on the deployment (not_found) and a team that has not opened its directory to apps (team_not_found).
This commit is contained in:
1 parent
27c4873042
commit
a145df46a2
6 files changed
+298
No files matched your search
@@ -941,6 +941,29 @@ const examples = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Teams',
|
||||
children: [
|
||||
{
|
||||
title: "Find the user's team",
|
||||
description: "Check whether the signed-in user belongs to a team with Puter.js teams API. Run and experiment with this example directly in the playground.",
|
||||
slug: 'teams-list',
|
||||
source: '/playground/examples/teams-list.html',
|
||||
},
|
||||
{
|
||||
title: 'List team members',
|
||||
description: 'List the accounts belonging to a team with Puter.js teams API. Run and modify this example instantly in your browser.',
|
||||
slug: 'teams-list-members',
|
||||
source: '/playground/examples/teams-list-members.html',
|
||||
},
|
||||
{
|
||||
title: 'Offer colleagues in a picker',
|
||||
description: 'Build a colleague picker from a team directory with Puter.js teams API. Run and experiment with this example in the playground.',
|
||||
slug: 'teams-directory',
|
||||
source: '/playground/examples/teams-directory.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Workers',
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<html>
|
||||
<body>
|
||||
<script src="https://js.puter.com/v2/"></script>
|
||||
<select id="colleagues"><option>Loading…</option></select>
|
||||
<button id="pick">Pick</button>
|
||||
<script>
|
||||
const select = document.getElementById('colleagues');
|
||||
|
||||
(async () => {
|
||||
let teams;
|
||||
try {
|
||||
teams = await puter.teams.list();
|
||||
} catch (e) {
|
||||
if (e.code !== 'not_found') throw e;
|
||||
select.innerHTML = '<option>Teams are not available here</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const team = teams[0];
|
||||
if (!team) {
|
||||
select.innerHTML = '<option>You are not on a team</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Off by default, and the usual reason the list comes back empty.
|
||||
if (!team.directoryEnabled) {
|
||||
select.innerHTML = '<option>Directory is closed to apps</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await puter.teams.listDirectory(team.uid);
|
||||
select.innerHTML = '';
|
||||
for (const { username, uuid } of entries) {
|
||||
const option = document.createElement('option');
|
||||
// Store the uuid: it survives a username change.
|
||||
option.value = uuid;
|
||||
option.textContent = username;
|
||||
select.append(option);
|
||||
}
|
||||
})();
|
||||
|
||||
document.getElementById('pick').addEventListener('click', () => {
|
||||
const option = select.selectedOptions[0];
|
||||
if (option) puter.print(`${option.textContent} → ${option.value}<br>`);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<html>
|
||||
<body>
|
||||
<script src="https://js.puter.com/v2/"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
let teams;
|
||||
try {
|
||||
teams = await puter.teams.list();
|
||||
} catch (e) {
|
||||
if (e.code !== 'not_found') throw e;
|
||||
puter.print('Teams are not available on this Puter.');
|
||||
return;
|
||||
}
|
||||
|
||||
const team = teams[0];
|
||||
if (!team) {
|
||||
puter.print('You are not on a team.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const members = await puter.teams.listMembers(team.uid);
|
||||
puter.print(`${members.length} member(s) of ${team.name}:<br>`);
|
||||
// An app gets `username` only — no email or activation state.
|
||||
for (const member of members) puter.print(`${member.username}<br>`);
|
||||
} catch (e) {
|
||||
if (e.code !== 'team_not_found') throw e;
|
||||
puter.print('This team has not opened its directory to apps.');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<html>
|
||||
<body>
|
||||
<script src="https://js.puter.com/v2/"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
let teams;
|
||||
try {
|
||||
teams = await puter.teams.list();
|
||||
} catch (e) {
|
||||
// No /teams route at all when the feature is off, so this
|
||||
// rejects rather than returning an empty list.
|
||||
if (e.code !== 'not_found') throw e;
|
||||
puter.print('Teams are not available on this Puter.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (teams.length === 0) {
|
||||
puter.print('You are not on a team.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const team of teams) {
|
||||
puter.print(`${team.name ?? team.handle ?? 'Unnamed team'}<br>`);
|
||||
puter.print(`uid: ${team.uid}<br>`);
|
||||
puter.print(`owner: ${team.isOwner}<br>`);
|
||||
puter.print(`directory open to apps: ${team.directoryEnabled}<br><br>`);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,7 @@ const recipeTags = {
|
||||
hosting: 'Hosting',
|
||||
workers: 'Workers',
|
||||
ui: 'UI',
|
||||
teams: 'Teams',
|
||||
performance: 'Performance',
|
||||
'data-modeling': 'Data Modeling',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Read the User's Team
|
||||
description: "Find out whether the signed-in user belongs to a team, show which one, and list their colleagues so your app can suggest them."
|
||||
tags: [teams, auth]
|
||||
order: 60
|
||||
---
|
||||
|
||||
**Use this when** your app should adapt to the team the signed-in user belongs
|
||||
to, such as labelling their workspace or offering colleagues in a picker
|
||||
instead of asking them to type usernames.
|
||||
|
||||
Everything here is read-only. Creating a team, adding accounts to it and paying
|
||||
for them are done by the team owner from their own Puter account, never from an
|
||||
app, so `puter.teams` gives an app three calls: [`list()`](/Teams/list/),
|
||||
[`listMembers()`](/Teams/listMembers/) and
|
||||
[`listDirectory()`](/Teams/listDirectory/).
|
||||
|
||||
## Check whether the user is on a team
|
||||
|
||||
`list()` returns the teams the signed-in user belongs to:
|
||||
|
||||
```js
|
||||
const teams = await puter.teams.list();
|
||||
|
||||
if (teams.length === 0) {
|
||||
console.log('Not on a team');
|
||||
} else {
|
||||
console.log(`On ${teams.length} team(s)`);
|
||||
}
|
||||
```
|
||||
|
||||
An empty array means *this user has no team*. It does not mean the deployment
|
||||
has no teams: where the feature is turned off there is no `/teams` route at
|
||||
all, so the call rejects with `not_found` instead of resolving. Handle the two
|
||||
separately, or a user on a Puter without teams looks identical to one who
|
||||
simply has not joined anything:
|
||||
|
||||
```js
|
||||
let teams = [];
|
||||
try {
|
||||
teams = await puter.teams.list();
|
||||
} catch (e) {
|
||||
if (e.code !== 'not_found') throw e;
|
||||
// Teams aren't available here — hide the team parts of your UI.
|
||||
}
|
||||
```
|
||||
|
||||
Failures reject with an error carrying a `code`, so `e.code` is what you branch
|
||||
on.
|
||||
|
||||
## Show which team
|
||||
|
||||
A team carries a `name` and a `handle`, and either may be `null`:
|
||||
|
||||
```js
|
||||
const [team] = await puter.teams.list();
|
||||
|
||||
console.log(team.name ?? team.handle ?? 'Unnamed team');
|
||||
console.log(team.createdAt); // '2026-09-28T10:04:00Z'
|
||||
console.log(team.isOwner); // true if this user owns the team
|
||||
```
|
||||
|
||||
Pass `team.uid` to the other two methods, and store that if you store anything.
|
||||
Never key on `handle`: it is a label the owner can rename, and deleting a team
|
||||
releases it for someone else to take, so a saved handle can later resolve to a
|
||||
different team. `uid` is the stable reference.
|
||||
|
||||
## List the user's colleagues
|
||||
|
||||
`listMembers()` takes the team's `uid`. Any member may call it:
|
||||
|
||||
```js
|
||||
const [team] = await puter.teams.list();
|
||||
const members = await puter.teams.listMembers(team.uid);
|
||||
|
||||
for (const member of members) {
|
||||
console.log(member.username);
|
||||
}
|
||||
```
|
||||
|
||||
From an app each entry carries `username` and nothing else — no email, no
|
||||
activation state, no usage. The extra fields you will see in the type
|
||||
(`orgOwned`, `createdAt`, `uuid`) are filled in only when the user's own Puter
|
||||
session makes the call, so write your UI against `username` alone.
|
||||
|
||||
## Offer colleagues in a picker
|
||||
|
||||
`listDirectory()` is the one built for suggesting people. It returns each
|
||||
colleague's `username` together with a `uuid` that survives a rename, which is
|
||||
what you want to store against a share or an assignment:
|
||||
|
||||
```js
|
||||
const entries = await puter.teams.listDirectory(team.uid);
|
||||
|
||||
for (const { username, uuid } of entries) {
|
||||
addOption(username, uuid);
|
||||
}
|
||||
```
|
||||
|
||||
Accounts that are suspended, or that never signed in for the first time, are
|
||||
left out, so the list is people your user can actually reach.
|
||||
|
||||
## The team has to opt in
|
||||
|
||||
An app sees a team only once that team's owner has turned its directory on.
|
||||
This is off by default, and it is the single most common reason these calls
|
||||
come back empty from an app but full from the user's own Puter session.
|
||||
|
||||
The behaviour differs per call, which is worth knowing when you are debugging:
|
||||
|
||||
- `list()` **omits** teams that have not opted in. It does not throw — a user
|
||||
on one closed team gets `[]`.
|
||||
- `listMembers()` and `listDirectory()` **reject** with `team_not_found`.
|
||||
|
||||
`directoryEnabled` on the team tells you which case you are in, so you can say
|
||||
something useful instead of showing an empty list:
|
||||
|
||||
```js
|
||||
const [team] = await puter.teams.list();
|
||||
|
||||
if (!team.directoryEnabled) {
|
||||
console.log('Ask the team owner to turn on the directory.');
|
||||
} else {
|
||||
const entries = await puter.teams.listDirectory(team.uid);
|
||||
}
|
||||
```
|
||||
|
||||
## Longer lists
|
||||
|
||||
All three methods share the same three forms. By default you get the whole list
|
||||
as an array, which is what every example above uses:
|
||||
|
||||
```js
|
||||
const members = await puter.teams.listMembers(team.uid);
|
||||
```
|
||||
|
||||
Passing `cursor` or `includeTotal` switches to a page envelope instead:
|
||||
|
||||
```js
|
||||
const page = await puter.teams.listMembers(team.uid, { limit: 50, cursor: null });
|
||||
|
||||
page.items; // this page's members
|
||||
page.cursor; // present only while more pages remain
|
||||
```
|
||||
|
||||
And `stream: true` hands back an async iterator of those envelopes, for walking
|
||||
a large team without holding it all at once:
|
||||
|
||||
```js
|
||||
for await (const page of puter.teams.listMembers(team.uid, { stream: true })) {
|
||||
for (const member of page.items) console.log(member.username);
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- These routes are keyset-paginated, so `offset` is rejected rather than
|
||||
silently ignored. Pass `cursor` to resume from a position.
|
||||
- Everything is scoped to the signed-in person's own membership, so an app
|
||||
installed by a member of one team can never read another team.
|
||||
- A user may belong to more than one team. `list()[0]` is fine for a demo, but
|
||||
let the user choose if your app acts on a specific one.
|
||||
Reference in new issue
Block a user