Add members invite, role change, and remove CRUD - #2
Conversation
Tenant admins can manage memberships on /members with last-admin and self-remove guards, audit events, and inline UI matching Environments. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Code Review
This pull request implements the Members CRUD functionality, allowing tenant administrators to invite members, update their roles, and remove them. The backend changes introduce a new MemberService, MemberPolicy, and corresponding controller endpoints with audit logging and feature tests. On the frontend, MembersPage.vue is updated with inline forms for inviting and editing members, alongside localization support and updated TypeScript types. The review feedback highlights several dark mode styling omissions in the new frontend form and table components, which can cause readability and contrast issues on dark backgrounds.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| <label v-if="!editingId" class="block"> | ||
| <span class="mb-1 block text-sm font-medium text-gray-700">{{ | ||
| $t('settings.members.fields.email') | ||
| }}</span> | ||
| <input | ||
| v-model="form.email" | ||
| type="email" | ||
| required | ||
| class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm" | ||
| /> | ||
| </label> | ||
| <label v-if="!editingId" class="block"> | ||
| <span class="mb-1 block text-sm font-medium text-gray-700">{{ | ||
| $t('settings.members.fields.name') | ||
| }}</span> | ||
| <input | ||
| v-model="form.name" | ||
| class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm" | ||
| /> | ||
| <span class="mt-1 block text-xs text-gray-500">{{ | ||
| $t('settings.members.fields.nameHint') | ||
| }}</span> | ||
| </label> |
There was a problem hiding this comment.
The form labels and input fields lack dark mode classes. In dark mode, the form container has a dark background (dark:bg-gray-900), but these inputs will remain white with low-contrast labels, making them hard to read and visually inconsistent. Let's add appropriate Tailwind dark mode classes (dark:text-gray-300, dark:border-gray-700, dark:bg-gray-800, dark:text-gray-100) to ensure a seamless dark mode experience.
<label v-if="!editingId" class="block">
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
$t('settings.members.fields.email')
}}</span>
<input
v-model="form.email"
type="email"
required
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
</label>
<label v-if="!editingId" class="block">
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
$t('settings.members.fields.name')
}}</span>
<input
v-model="form.name"
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
<span class="mt-1 block text-xs text-gray-500 dark:text-gray-400">{{
$t('settings.members.fields.nameHint')
}}</span>
</label>
| <label class="block"> | ||
| <span class="mb-1 block text-sm font-medium text-gray-700">{{ | ||
| $t('settings.members.fields.role') | ||
| }}</span> | ||
| <select | ||
| v-model="form.role" | ||
| required | ||
| class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm" | ||
| > | ||
| <option v-for="role in ROLE_OPTIONS" :key="role" :value="role"> | ||
| {{ roleLabel(role) }} | ||
| </option> | ||
| </select> | ||
| </label> |
There was a problem hiding this comment.
Similarly, the role select element and its label should also support dark mode to match the rest of the form.
<label class="block">
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{
$t('settings.members.fields.role')
}}</span>
<select
v-model="form.role"
required
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
>
<option v-for="role in ROLE_OPTIONS" :key="role" :value="role">
{{ roleLabel(role) }}
</option>
</select>
</label>
| <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-800 dark:bg-gray-950"> | ||
| <tr v-for="member in data" :key="member.id"> | ||
| <td class="px-4 py-3 font-medium">{{ member.name }}</td> | ||
| <td class="px-4 py-3 text-sm text-gray-600">{{ member.email }}</td> |
There was a problem hiding this comment.
Summary
Test plan
Made with Cursor