Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 101 additions & 5 deletions src/api/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,11 +652,9 @@ export async function logout() {
// cross-origin URL the dev proxy can't follow, so the browser reports a
// network error even though we're logged out. Handle it in LogoutPage
// instead of surfacing the global connection-error modal.
const res = await axios.post(
`${BASE_URL}/loginManager/logout`,
undefined,
{ skipErrorNotifications: true } as CustomAxiosRequestConfig
);
const res = await axios.post(`${BASE_URL}/loginManager/logout`, undefined, {
skipErrorNotifications: true,
} as CustomAxiosRequestConfig);
return res.data;
}

Expand Down Expand Up @@ -1509,3 +1507,101 @@ export async function updateDrawerGroup(
export async function deleteDrawerGroup(groupId: number): Promise<void> {
await axios.delete(`${BASE_URL}/drawerPermissions/groups/${groupId}`);
}

// The drawer-manager twin of fetchUserAutocomplete: same shape, but the
// endpoint is open to any drawer manager, not just instance admins.
export async function fetchDrawerUserAutocomplete(
query: string,
options?: { signal?: AbortSignal }
): Promise<UserAutocompleteMatch[]> {
const res = await axios.get<{ matches: UserAutocompleteMatch[] }>(
`${BASE_URL}/drawerPermissions/userAutocomplete`,
{ params: { q: query }, signal: options?.signal }
);

return res.data.matches;
}

export async function fetchDrawerGroupMembers(
groupId: number
): Promise<GroupMember[]> {
const res = await axios.get<{ members: GroupMember[] }>(
`${BASE_URL}/drawerPermissions/groups/${groupId}/members`
);

return res.data.members;
}

export async function addDrawerGroupMember(
input: AddGroupMemberInput
): Promise<GroupMember> {
const params = new URLSearchParams();
if ("localUserId" in input) {
params.append("localUserId", String(input.localUserId));
} else {
params.append("remoteUserId", input.remoteUserId);
}

const res = await axios.post<{ member: GroupMember }>(
`${BASE_URL}/drawerPermissions/groups/${input.groupId}/members`,
params
);

return res.data.member;
}

export async function removeDrawerGroupMember(
groupId: number,
userId: number
): Promise<void> {
await axios.delete(
`${BASE_URL}/drawerPermissions/groups/${groupId}/members/${userId}`
);
}

export async function fetchDrawerGroupEntries(
groupId: PermissionsGroup["id"]
): Promise<PermissionsGroupEntry[]> {
const res = await axios.get<{ entries: PermissionsGroupEntry[] }>(
`${BASE_URL}/drawerPermissions/groups/${groupId}/entries`
);
return res.data.entries;
}

export async function addDrawerGroupEntry({
groupId,
value,
}: AddGroupEntryInput): Promise<PermissionsGroupEntry> {
const payload = new URLSearchParams({ value });

const res = await axios.post<{ entry: PermissionsGroupEntry }>(
`${BASE_URL}/drawerPermissions/groups/${groupId}/entries`,
payload
);

return res.data.entry;
}

export async function updateDrawerGroupEntry({
groupId,
entryId,
value,
}: UpdateGroupEntryInput): Promise<PermissionsGroupEntry> {
const payload = new URLSearchParams({ value });

const res = await axios.put<{ entry: PermissionsGroupEntry }>(
`${BASE_URL}/drawerPermissions/groups/${groupId}/entries/${entryId}`,
payload
);

return res.data.entry;
}

export async function removeDrawerGroupEntry(
groupId: PermissionsGroup["id"],
entryId: PermissionsGroupEntry["id"]
): Promise<void> {
await axios.delete(
`${BASE_URL}/drawerPermissions/groups/${groupId}/entries/${entryId}`
);
}
9 changes: 6 additions & 3 deletions src/pages/AdminPermissionsPage/GroupsTabContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@
v-for="header in headerGroup.headers"
:key="header.id"
class="bg-surface-container-low"
:class="{
'cursor-pointer select-none': header.column.getCanSort(),
}"
:class="[
header.column.columnDef.meta?.widthClass,
{
'cursor-pointer select-none': header.column.getCanSort(),
},
]"
@click="header.column.getToggleSortingHandler()?.($event)">
<div class="flex items-center gap-2">
<FlexRender
Expand Down
9 changes: 6 additions & 3 deletions src/pages/AdminPermissionsPage/RulesTabContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@
v-for="header in headerGroup.headers"
:key="header.id"
class="bg-surface-container-low"
:class="{
'cursor-pointer select-none': header.column.getCanSort(),
}"
:class="[
header.column.columnDef.meta?.widthClass,
{
'cursor-pointer select-none': header.column.getCanSort(),
},
]"
@click="header.column.getToggleSortingHandler()?.($event)">
<div class="flex items-center gap-2">
<FlexRender
Expand Down
94 changes: 94 additions & 0 deletions src/pages/DrawerManagementPage/AddGroupEntryRow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<template>
<TableRow v-if="isPending || isOpen">
<TableCell class="text-sm p-2" :colspan="colspan">
<p v-if="isPending">{{ pendingValue }} (saving...)</p>
<form
v-else
class="flex items-center gap-2"
:data-group-entry-add-form="group.id"
@submit.prevent="handleSave">
<GroupEntryValueInput
v-model="draftValue"
class="flex-1"
:group="group"
:inputId="`group-entry-add-input-${group.id}`"
:inputClass="`group-entry-add__input--${group.id}`"
:label="`New ${group.label} Entry Value`" />
<Button
type="submit"
:data-group-entry-add-save="group.id"
variant="secondary"
class="py-2 border border-secondary-container"
:aria-label="`Save new entry in ${group.label}`">
<CheckIcon class="size-4" />
Save
</Button>
<Button
type="button"
:data-group-entry-add-cancel="group.id"
variant="tertiary"
class="py-2"
:aria-label="`Cancel new entry in ${group.label}`"
@click="closeForm">
<XIcon class="size-4" />
Cancel
</Button>
</form>
</TableCell>
</TableRow>
</template>
<script setup lang="ts">
import type { PermissionsGroup } from "@/types";
import { TableRow, TableCell } from "@/components/ui/table";
import { computed, ref } from "vue";
import { XIcon, CheckIcon } from "lucide-vue-next";
import GroupEntryValueInput from "./GroupEntryValueInput.vue";
import { useAddDrawerGroupEntryMutation } from "./drawerGroupQueries";
import Button from "@/components/Button/Button.vue";

const props = withDefaults(
defineProps<{ group: PermissionsGroup; colspan?: number }>(),
{
colspan: 2,
}
);

// The in-flight "(saving...)" row renders here from the mutation, so the
// parent toggles `open` instead of v-if. Unmounting would drop that row
// while the add is still settling.
const isOpen = defineModel<boolean>("open", { required: true });

const draftValue = ref("");

const {
mutate: addGroupEntry,
isPending,
variables,
} = useAddDrawerGroupEntryMutation();

// Show what was submitted, not the live input the user may keep editing.
const pendingValue = computed(() => variables.value?.value ?? "");

function closeForm(): void {
isOpen.value = false;
draftValue.value = "";
}

function handleSave(): void {
const trimmedDraft = draftValue.value.trim();

// a blank draft has nothing to save, treat it as a cancel
if (trimmedDraft === "") {
closeForm();
return;
}

// Close only on success so a failed add reopens the form with the
// typed value still in place for a retry.
addGroupEntry(
{ groupId: props.group.id, value: trimmedDraft },
{ onSuccess: closeForm }
);
}
</script>
<style scoped></style>
Loading
Loading