Skip to content

Commit

Permalink
[studio] create user table #1516 (#1537)
Browse files Browse the repository at this point in the history
* studio - added user table

* studio - user table improvements

* studio - user table unit tests

* studio - remove console log

* studio - renamed user action remove to delete
  • Loading branch information
janavlachova authored Jan 26, 2025
1 parent 3cac3eb commit 6babd1f
Show file tree
Hide file tree
Showing 16 changed files with 777 additions and 7 deletions.
24 changes: 23 additions & 1 deletion agdb_studio/src/components/base/content/AgdbContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,25 @@ onMounted(() => {
<div v-if="part.component">
<component :is="part.component" />
</div>
<div v-if="part.input" class="input-row">
<div v-if="part.input && part.input.type === 'checkbox'">
<input
class="checkbox"
:name="part.input.key"
:type="part.input.type"
:checked="getInputValue(props.contentKey, part.input.key)"
@change="
(event: Event) => {
setInputValue(
props.contentKey,
part.input?.key,
(event.target as HTMLInputElement).checked,
);
}
"
/>
<label :for="part.input.key">{{ part.input.label }}</label>
</div>
<div v-else-if="part.input" class="input-row">
<label :for="part.input.key">{{ part.input.label }}</label>
<div :class="{ 'error-input': part.input.error }">
<select
Expand Down Expand Up @@ -141,6 +159,10 @@ onMounted(() => {
max-width: 40%;
}
.checkbox {
margin-right: 0.5rem;
}
@media (max-width: 768px) {
.input-row {
grid-template-columns: 1fr;
Expand Down
61 changes: 61 additions & 0 deletions agdb_studio/src/components/base/table/AgdbCell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,65 @@ describe("AgdbCell", () => {
});
expect(wrapper.html()).toContain("agdb-cell-menu-stub");
});
it("should render boolean cell when it is true", () => {
const columns = new Map();
columns.set("backup", {
key: "backup",
title: "Backup",
type: "boolean",
});
const wrapper = mount(AgdbCell, {
props: {
cellKey: "backup",
},
global: {
provide: {
[INJECT_KEY_COLUMNS]: { value: columns },
[INJECT_KEY_ROW]: {
value: {
role: "admin",
owner: "admin",
db: "test",
db_type: "memory",
size: 2656,
backup: true,
},
},
},
},
});

expect(wrapper.find(".agdb-cell .positive-icon").exists()).toBe(true);
});

it("should render boolean cell when it is false", () => {
const columns = new Map();
columns.set("backup", {
key: "backup",
title: "Backup",
type: "boolean",
});
const wrapper = mount(AgdbCell, {
props: {
cellKey: "backup",
},
global: {
provide: {
[INJECT_KEY_COLUMNS]: { value: columns },
[INJECT_KEY_ROW]: {
value: {
role: "admin",
owner: "admin",
db: "test",
db_type: "memory",
size: 2656,
backup: false,
},
},
},
},
});

expect(wrapper.find(".agdb-cell .negative-icon").exists()).toBe(true);
});
});
14 changes: 13 additions & 1 deletion agdb_studio/src/components/base/table/AgdbCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import type { Column, TRow } from "@/composables/table/types";
import { computed, inject, type Ref } from "vue";
import AgdbCellMenu from "./AgdbCellMenu.vue";
import { BsCheckLg, ClCloseMd } from "@kalimahapps/vue-icons";
const props = defineProps({
cellKey: {
Expand Down Expand Up @@ -37,10 +38,21 @@ const formattedValue = computed(() => {
<div v-else-if="column?.actions">
<AgdbCellMenu :actions="column.actions" />
</div>
<div v-else-if="column?.type === 'boolean'">
<BsCheckLg v-if="value" class="positive-icon" title="Yes" />
<ClCloseMd v-else class="negative-icon" title="No" />
</div>
<div v-else>
<p>{{ formattedValue }}</p>
</div>
</div>
</template>

<style lang="less" scoped></style>
<style lang="less" scoped>
.positive-icon {
color: var(--success-color-2);
}
.negative-icon {
color: var(--error-color-2);
}
</style>
47 changes: 47 additions & 0 deletions agdb_studio/src/components/user/UserAddForm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { vi, describe, it, beforeEach, expect } from "vitest";
import UserAddForm from "./UserAddForm.vue";
import { mount } from "@vue/test-utils";

const { addUser } = vi.hoisted(() => {
return {
addUser: vi.fn(),
};
});

vi.mock("@/composables/user/userStore", () => {
return {
useUserStore: () => {
return {
addUser,
};
},
};
});

describe("UserAddForm", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should add a user when user submits", async () => {
addUser.mockResolvedValueOnce(true);
expect(addUser).not.toHaveBeenCalled();
const wrapper = mount(UserAddForm);
await wrapper.find("input#username").setValue("test_user");
await wrapper.find("input#password").setValue("test_password");
await wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
expect(addUser).toHaveBeenCalledOnce();
});

it("should add a user when user clicks submit button", async () => {
addUser.mockResolvedValueOnce(true);
expect(addUser).not.toHaveBeenCalled();
const wrapper = mount(UserAddForm);
await wrapper.find("input#username").setValue("test_user");
await wrapper.find("input#password").setValue("test_password");
await wrapper.find("button[type=submit]").trigger("click");
await wrapper.vm.$nextTick();
expect(addUser).toHaveBeenCalledOnce();
});
});
62 changes: 62 additions & 0 deletions agdb_studio/src/components/user/UserAddForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script lang="ts" setup>
import { ref } from "vue";
import { useUserStore } from "@/composables/user/userStore";
const username = ref("");
const password = ref("");
const loading = ref(false);
const { addUser, fetchUsers } = useUserStore();
const add = (event: Event) => {
loading.value = true;
event.preventDefault();
addUser({
username: username.value,
password: password.value,
})
.then(() => {
loading.value = false;
username.value = "";
password.value = "";
fetchUsers();
})
.catch(() => {
loading.value = false;
});
};
</script>

<template>
<div class="user-add-form">
<h2>Add User</h2>
<form id="user-add-form" @submit="add">
<div class="form-group">
<label for="username">Username</label>
<input id="username" v-model="username" type="text" />
</div>
<div class="form-group">
<label for="password">Password</label>
<input id="password" v-model="password" type="text" />
</div>
<button type="submit" class="button" @click="add">Add User</button>
</form>
</div>
</template>

<style lang="less" scoped>
.user-add-form {
margin: 1rem auto;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
}
.form-group {
margin: 0.5rem 0;
display: flex;
flex-direction: column;
align-items: flex-start;
}
</style>
46 changes: 46 additions & 0 deletions agdb_studio/src/components/user/UserTable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { vi, describe, it, beforeEach, expect } from "vitest";
import UserTable from "./UserTable.vue";
import { mount, shallowMount } from "@vue/test-utils";

const { users } = vi.hoisted(() => {
return {
users: {
value: [
{
username: "test_user",
admin: false,
login: false,
},
{
username: "test_user2",
admin: false,
login: false,
},
],
},
};
});

vi.mock("@/composables/user/userStore", () => {
return {
useUserStore: () => {
return { users };
},
};
});

describe("UserTable", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("renders", () => {
const wrapper = shallowMount(UserTable);
expect(wrapper.exists()).toBe(true);
});
it("should render message when no users", () => {
users.value = [];
const wrapper = mount(UserTable);
expect(wrapper.text()).toContain("No users found");
});
});
42 changes: 42 additions & 0 deletions agdb_studio/src/components/user/UserTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts" setup>
import { addTable } from "@/composables/table/tableConfig";
import { setTableData } from "@/composables/table/tableData";
import { userColumns } from "@/composables/user/userConfig";
import { useUserStore } from "@/composables/user/userStore";
import { watchEffect } from "vue";
import AgdbTable from "../base/table/AgdbTable.vue";
const { users } = useUserStore();
const TABLE_KEY = Symbol("users");
addTable({
name: TABLE_KEY,
columns: userColumns,
uniqueKey: "username",
});
watchEffect(() => {
setTableData(TABLE_KEY, users.value);
});
</script>

<template>
<div class="table-wrap">
<div v-if="users.length" class="user-table">
<AgdbTable :name="TABLE_KEY" />
</div>

<p v-else>No users found</p>
</div>
</template>

<style lang="less" scoped>
.table-wrap {
overflow: auto;
}
.user-table {
width: 700px;
margin: 0 auto;
}
</style>
1 change: 1 addition & 0 deletions agdb_studio/src/composables/db/dbStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe("DbStore", () => {
it("fetches databases when called", async () => {
const { databases, fetchDatabases } = useDbStore();
await fetchDatabases();
expect(dbList).toHaveBeenCalledOnce();
expect(databases.value).toHaveLength(2);
});

Expand Down
11 changes: 11 additions & 0 deletions agdb_studio/src/composables/table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export type Column<T extends TRow> = {
cellComponent?: AsyncComponent | ((row: T) => AsyncComponent);
valueFormatter?: (value: TCellType) => TCellType;
actions?: Action<T>[];
type?: "string" | "number" | "boolean";

// TODO: possibly add these later
// width?: string;
// minWidth?: string;
// maxWidth?: string;
// align?: "left" | "center" | "right";
// headerAlign?: "left" | "center" | "right";
// headerComponent?: AsyncComponent;
// filterComponent?: AsyncComponent;
// filter?: (row: T, filter: string) => boolean;
};

export type Table<T extends TRow> = {
Expand Down
Loading

0 comments on commit 6babd1f

Please sign in to comment.