feat(projekter): del et projekt med en, der allerede er her
Et projekt kunne kun deles ved at stave en e-mailadresse. Det er den forkerte
vej at spørge om en kollega, man kender navnet på og sidder i samme rum som —
og beslutningen om, hvorvidt en instans' konti må vises for hinanden, var
allerede taget: `UsersForSharing` har gjort det for noter hele tiden, med
begrundelsen skrevet ved siden af. Der er ingen åben oprettelse, så konti er en
lukket, inviteret mængde, og det er en adressebog frem for en udlevering.
Projekterne bruger nu den samme.
`GET /members` svarer med tre lister frem for én, så panelet åbner færdigt i ét
kald — samme form som notens delingspanel. `POST /invites` tager `user_id` ved
siden af `email`, og de to veje mødes ét sted: en adresse, der viser sig at høre
til en konto, ER den konto, og skal ikke gennem en oprettelsesside, den ikke kan
komme igennem.
De ventende invitationer kom med, fordi den nye liste skabte behovet for dem:
den, der er inviteret og ikke kommet, har ingen konto og står derfor hverken
blandt medlemmerne eller blandt dem, man kan vælge. Før betød det, at man ikke
kunne se sin egen invitation; nu ville det betyde, at man leder efter en person,
der mangler i listen, og inviterer igen. Det andet link virker lige så godt som
det første. De kan trækkes tilbage, for ellers er den ventende liste kun til at
se på.
Tre ting, der kostede en kørsel hver:
`OpenAPI is valid` fangede den nye DELETE-rute, før jeg selv gjorde.
To felter hed "Rolle" på den samme side. Det er ikke kun en prøve, der brokker
sig — en skærmlæser siger "Rolle" to gange uden at kunne skelne. De har nu hver
sin tilstand og hvert sit navn, og den gamle prøves `getByLabel('Rolle')` er
blevet `exact`, fordi den matchede som understreng.
Og e2e-prøven lånte først sin anden person fra prøven ovenfor. Det så jeg kun,
fordi jeg kørte den alene: basen tømmes for hver kørsel, så en prøve, der kun
består, når hele filen kører i rækkefølge, består ikke af sine egne grunde. Den
laver nu selv sin — og viser dermed skiftet: den samme person er først en
ventende adresse og bagefter et navn, der kan vælges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a39f655565
commit
9b21b3eac5
+4
-3
@@ -96,9 +96,10 @@ curl -X POST https://todo.example.dk/api/v1/tasks/quick-add \
|
||||
| `GET` `PATCH` `DELETE` | `/projects/{id}` | Read, change, trash. Owner only for the last two. |
|
||||
| `GET` `POST` | `/projects/{id}/sections` | |
|
||||
| `PATCH` `DELETE` | `/sections/{id}` | |
|
||||
| `GET` | `/projects/{id}/members` | |
|
||||
| `POST` | `/projects/{id}/invites` | Share it. `{"email": …, "role": "editor"\|"viewer"}` |
|
||||
| `DELETE` | `/projects/{id}/members/{userID}` | |
|
||||
| `GET` | `/projects/{id}/members` | Who is on it, who can be added (`candidates`), and who has been invited and not arrived (`pending`). |
|
||||
| `POST` | `/projects/{id}/invites` | Share it. `{"user_id": …}` for somebody who already has an account here, `{"email": …}` for anybody else — plus `"role": "editor"\|"viewer"`. An address that turns out to belong to an account is that account. |
|
||||
| `DELETE` | `/projects/{id}/invites/{inviteID}` | Withdraw an invitation before it is used. |
|
||||
| `PATCH` `DELETE` | `/projects/{id}/members/{userID}` | Change somebody's role, or take them off. |
|
||||
| `GET` | `/projects/{id}/activity` | |
|
||||
| `GET` | `/trash/projects` | What you have deleted and can still bring back, with how long is left. |
|
||||
| `POST` | `/trash/projects/{id}/restore` | Brings it back with the tasks that went with it. |
|
||||
|
||||
+5
-2
@@ -40,8 +40,11 @@ forward mail to, and an MCP endpoint so Claude can work your task list directly.
|
||||
it is due.
|
||||
- **Drag where it means something** — reorder a list or a board, file a project
|
||||
under a group, drop a task on another project, or drop it on another day.
|
||||
- **Sharing** with owner, editor and viewer roles. Anyone outside your instance
|
||||
joins through an invite link.
|
||||
- **Sharing** with owner, editor and viewer roles. Somebody who already has an
|
||||
account here you pick by name — the accounts on an instance are a closed,
|
||||
invited set, so they are an address book rather than a directory to protect.
|
||||
Anyone outside your instance joins through an invite link, and the invitation
|
||||
stays visible until they arrive, so it does not get sent twice.
|
||||
- **Live sync** — a change made by somebody else appears without a refresh.
|
||||
- **[Told when it matters](notifications.md).** A bell for what other people did to
|
||||
what you share, pushed to your devices and counted on the app's icon in the Dock.
|
||||
|
||||
+64
-4
@@ -2307,7 +2307,15 @@ paths:
|
||||
- { name: projectID, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [projects]
|
||||
summary: Who the project is shared with.
|
||||
summary: Who the project is shared with, who can be added, and who has been invited.
|
||||
description: >
|
||||
Three lists in one answer, so the share panel opens complete in a single
|
||||
round-trip. `candidates` is everybody with an account here who is not
|
||||
already on the project — the instance's accounts are a closed, invited set,
|
||||
so listing them to each other is the address book the feature needs, not a
|
||||
disclosure. `pending` is the invitations sent to people who have no account
|
||||
yet: without them an invitation sent yesterday is in neither of the other
|
||||
two lists, and gets sent a second time.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
@@ -2319,6 +2327,17 @@ paths:
|
||||
members:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/Member" }
|
||||
candidates:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/Person" }
|
||||
pending:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
email: { type: string, format: email }
|
||||
role: { type: string, enum: [editor, viewer] }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
|
||||
/api/v1/projects/{projectID}/members/{userID}:
|
||||
@@ -2384,13 +2403,32 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [email, role]
|
||||
properties:
|
||||
email: { type: string, format: email }
|
||||
user_id:
|
||||
type: string
|
||||
description: >
|
||||
Somebody who already has an account here, picked from the
|
||||
candidates in GET /members. One of user_id or email.
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: >
|
||||
An address. If it belongs to an account they are added at
|
||||
once; otherwise an invitation is sent.
|
||||
role: { type: string, enum: [editor, viewer] }
|
||||
responses:
|
||||
"200":
|
||||
description: Added straight away — the person already had an account.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
added: { type: boolean }
|
||||
user: { $ref: "#/components/schemas/Person" }
|
||||
role: { type: string, enum: [editor, viewer] }
|
||||
"201":
|
||||
description: Created
|
||||
description: Invited. No account yet, so a link was made.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -2399,8 +2437,30 @@ paths:
|
||||
link: { type: string }
|
||||
emailed: { type: boolean }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"409": { $ref: "#/components/responses/Conflict" }
|
||||
"422": { $ref: "#/components/responses/Validation" }
|
||||
|
||||
/api/v1/projects/{projectID}/invites/{inviteID}:
|
||||
delete:
|
||||
tags: [projects]
|
||||
summary: Withdraw an invitation to a project before it is used. Its owner only.
|
||||
description: >
|
||||
The only way back. Only the token's hash is stored, so a link sent to the
|
||||
wrong address cannot be looked up and invalidated — only the row it belongs
|
||||
to can.
|
||||
parameters:
|
||||
- name: projectID
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: inviteID
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204": { description: Withdrawn }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
|
||||
/api/v1/projects/{projectID}/activity:
|
||||
parameters:
|
||||
- { name: projectID, in: path, required: true, schema: { type: string } }
|
||||
|
||||
@@ -8,6 +8,28 @@ What the first account is created with. Nothing is emailed, nothing is registere
|
||||
anywhere: the first person to open a fresh instance creates the administrator
|
||||
account, and after that people arrive by invite link.
|
||||
|
||||
## Being let into a project
|
||||
|
||||
Two different situations, one panel — **the project's ⋯ menu → Share**.
|
||||
|
||||
Somebody who **already has an account here** you pick from a list by name. You do
|
||||
not need to know their email address to share a project with a colleague whose
|
||||
name you know. The accounts on an instance are a closed set — there is no open
|
||||
signup, every one of them was invited — so listing them to each other is the
|
||||
address book the feature needs, not a disclosure.
|
||||
|
||||
Somebody who is **not here yet** you invite by email. If the address turns out to
|
||||
belong to an account it becomes an ordinary share straight away; otherwise it
|
||||
becomes a link that creates their account and their membership at once. With no
|
||||
mail server configured the link is shown in the panel instead of being sent, which
|
||||
is the correct behaviour on a one-person instance rather than an error.
|
||||
|
||||
An invitation that has not been taken up stays listed under the members, greyed,
|
||||
with a × to withdraw it. Without that it is invisible — the person has no account,
|
||||
so they are neither a member nor somebody you can pick — and the usual next move
|
||||
is to send it a second time. The second link works exactly as well as the first,
|
||||
and then there are two to keep track of.
|
||||
|
||||
## A code on top of it
|
||||
|
||||
**Settings → Account → Two-factor.** An ordinary TOTP secret — any authenticator
|
||||
|
||||
Reference in New Issue
Block a user