diff --git a/agdb_studio/package-lock.json b/agdb_studio/package-lock.json
index ede0229d8..8d4c4da76 100644
--- a/agdb_studio/package-lock.json
+++ b/agdb_studio/package-lock.json
@@ -9,6 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@kalimahapps/vue-icons": "^1.7.1",
+ "@vueuse/components": "^12.0.0",
+ "@vueuse/core": "^12.0.0",
"agdb_api": "file:../agdb_api/typescript",
"openapi-client-axios": "^7.5.1",
"vue": "^3.3.4",
@@ -1305,6 +1307,11 @@
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"dev": true
},
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
@@ -1699,12 +1706,12 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz",
- "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
+ "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dependencies": {
"@babel/parser": "^7.25.3",
- "@vue/shared": "3.5.12",
+ "@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
@@ -1716,27 +1723,27 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz",
- "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
+ "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dependencies": {
- "@vue/compiler-core": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/compiler-core": "3.5.13",
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz",
- "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
+ "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"dependencies": {
"@babel/parser": "^7.25.3",
- "@vue/compiler-core": "3.5.12",
- "@vue/compiler-dom": "3.5.12",
- "@vue/compiler-ssr": "3.5.12",
- "@vue/shared": "3.5.12",
+ "@vue/compiler-core": "3.5.13",
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/compiler-ssr": "3.5.13",
+ "@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
- "postcss": "^8.4.47",
+ "postcss": "^8.4.48",
"source-map-js": "^1.2.0"
}
},
@@ -1746,12 +1753,12 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz",
- "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
+ "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"dependencies": {
- "@vue/compiler-dom": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-vue2": {
@@ -1832,49 +1839,49 @@
}
},
"node_modules/@vue/reactivity": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz",
- "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
+ "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dependencies": {
- "@vue/shared": "3.5.12"
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz",
- "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
+ "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"dependencies": {
- "@vue/reactivity": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/reactivity": "3.5.13",
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz",
- "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
+ "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"dependencies": {
- "@vue/reactivity": "3.5.12",
- "@vue/runtime-core": "3.5.12",
- "@vue/shared": "3.5.12",
+ "@vue/reactivity": "3.5.13",
+ "@vue/runtime-core": "3.5.13",
+ "@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz",
- "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
+ "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"dependencies": {
- "@vue/compiler-ssr": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/compiler-ssr": "3.5.13",
+ "@vue/shared": "3.5.13"
},
"peerDependencies": {
- "vue": "3.5.12"
+ "vue": "3.5.13"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz",
- "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg=="
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
+ "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="
},
"node_modules/@vue/test-utils": {
"version": "2.4.6",
@@ -1892,6 +1899,49 @@
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
"dev": true
},
+ "node_modules/@vueuse/components": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-12.0.0.tgz",
+ "integrity": "sha512-XpOoBXYRuFuUiiq+HsMX6rGzqvcHdKnbT4sbR0FHYxwSGBHO3Zli8pPTZoLRNBGp4CGov7BRCnANEK/1Ch/6tQ==",
+ "dependencies": {
+ "@vueuse/core": "12.0.0",
+ "@vueuse/shared": "12.0.0",
+ "vue": "^3.5.13"
+ }
+ },
+ "node_modules/@vueuse/core": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
+ "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "12.0.0",
+ "@vueuse/shared": "12.0.0",
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
+ "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
+ "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
+ "dependencies": {
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -4115,9 +4165,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
@@ -4134,7 +4184,7 @@
],
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -5065,15 +5115,15 @@
"dev": true
},
"node_modules/vue": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz",
- "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==",
- "dependencies": {
- "@vue/compiler-dom": "3.5.12",
- "@vue/compiler-sfc": "3.5.12",
- "@vue/runtime-dom": "3.5.12",
- "@vue/server-renderer": "3.5.12",
- "@vue/shared": "3.5.12"
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
+ "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/compiler-sfc": "3.5.13",
+ "@vue/runtime-dom": "3.5.13",
+ "@vue/server-renderer": "3.5.13",
+ "@vue/shared": "3.5.13"
},
"peerDependencies": {
"typescript": "*"
diff --git a/agdb_studio/package.json b/agdb_studio/package.json
index a64f189fd..ca3477a4c 100644
--- a/agdb_studio/package.json
+++ b/agdb_studio/package.json
@@ -17,6 +17,8 @@
},
"dependencies": {
"@kalimahapps/vue-icons": "^1.7.1",
+ "@vueuse/components": "^12.0.0",
+ "@vueuse/core": "^12.0.0",
"agdb_api": "file:../agdb_api/typescript",
"openapi-client-axios": "^7.5.1",
"vue": "^3.3.4",
diff --git a/agdb_studio/src/assets/base.css b/agdb_studio/src/assets/base.css
index 37518dcf1..8e480440e 100644
--- a/agdb_studio/src/assets/base.css
+++ b/agdb_studio/src/assets/base.css
@@ -22,12 +22,15 @@
--text-dark-2: rgba(235, 235, 235, 0.64);
--base-font: "Red Hat Display", sans-serif;
+
+ --orange: #ffa02c;
}
:root {
--color-background: var(--white);
--color-background-soft: var(--white-soft);
--color-background-mute: var(--white-mute);
+ --color-background-active: var(--orange);
--color-border: var(--divider-light-2);
--color-border-hover: var(--divider-light-1);
diff --git a/agdb_studio/src/assets/button.less b/agdb_studio/src/assets/button.less
index 9fcc8a14f..b988ce18a 100644
--- a/agdb_studio/src/assets/button.less
+++ b/agdb_studio/src/assets/button.less
@@ -21,25 +21,25 @@
.button-default {
--backgroundColor: #007bff;
- --color: #fff;
+ --color: var(--white);
--borderColor: #025ec0;
}
.button-success {
--backgroundColor: #28a745;
- --color: #fff;
+ --color: var(--white);
--borderColor: #1e7732;
}
.button-warning {
- --backgroundColor: #ffa02c;
- --color: #181818;
+ --backgroundColor: var(--orange);
+ --color: var(--black);
--borderColor: #d88621;
}
.button-danger {
--backgroundColor: #dc3545;
- --color: #fff;
+ --color: var(--white);
--borderColor: #af2836;
}
diff --git a/agdb_studio/src/components/base/dropdown/AgdbDropdown.spec.ts b/agdb_studio/src/components/base/dropdown/AgdbDropdown.spec.ts
new file mode 100644
index 000000000..e39811c63
--- /dev/null
+++ b/agdb_studio/src/components/base/dropdown/AgdbDropdown.spec.ts
@@ -0,0 +1,44 @@
+import { mount } from "@vue/test-utils";
+import AgdbDropdown from "./AgdbDropdown.vue";
+import { describe, beforeEach, vi, it, expect } from "vitest";
+
+describe("AgdbDropdown", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+ it("should open and close on click", async () => {
+ const wrapper = mount(AgdbDropdown, {
+ slots: {
+ content: "
content
",
+ trigger: "trigger
",
+ },
+ });
+ const trigger = wrapper.find(".trigger");
+ expect(wrapper.find(".content").exists()).toBe(false);
+ trigger.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").isVisible()).toBe(true);
+ trigger.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").exists()).toBe(false);
+ });
+
+ it("should close when clicking outside", async () => {
+ const wrapper = mount(AgdbDropdown, {
+ slots: {
+ content: "content
",
+ trigger: "trigger
",
+ },
+ });
+ const trigger = wrapper.find(".trigger");
+ expect(wrapper.find(".content").exists()).toBe(false);
+ trigger.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").isVisible()).toBe(true);
+ document.body.click();
+ await wrapper.vm.$nextTick();
+ document.body.click();
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").exists()).toBe(false);
+ });
+});
diff --git a/agdb_studio/src/components/base/dropdown/AgdbDropdown.vue b/agdb_studio/src/components/base/dropdown/AgdbDropdown.vue
new file mode 100644
index 000000000..4fdb87f0a
--- /dev/null
+++ b/agdb_studio/src/components/base/dropdown/AgdbDropdown.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/agdb_studio/src/components/base/menu/AgdbMenu.spec.ts b/agdb_studio/src/components/base/menu/AgdbMenu.spec.ts
new file mode 100644
index 000000000..d3565cdb4
--- /dev/null
+++ b/agdb_studio/src/components/base/menu/AgdbMenu.spec.ts
@@ -0,0 +1,43 @@
+import { describe, beforeEach, vi, it, expect } from "vitest";
+import { mount } from "@vue/test-utils";
+import AgdbMenu from "./AgdbMenu.vue";
+import { dbActions } from "@/composables/db/dbConfig";
+import { db_backup } from "@/tests/apiMock";
+
+describe("AgdbMenu", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+ it("should run action on click", () => {
+ const wrapper = mount(AgdbMenu, {
+ props: {
+ actions: dbActions,
+ },
+ });
+
+ expect(wrapper.find(".agdb-menu").exists()).toBe(true);
+ expect(wrapper.find(".agdb-menu").text()).toContain("Convert");
+
+ const backup = wrapper.find(".menu-item[data-key='backup']");
+ backup.trigger("click");
+ expect(db_backup).toHaveBeenCalled();
+ });
+
+ it("should render the sub menu on hover", async () => {
+ const wrapper = mount(AgdbMenu, {
+ props: {
+ actions: dbActions,
+ },
+ });
+
+ const convert = wrapper.find(".menu-item[data-key='convert']");
+ await convert.trigger("mouseover");
+
+ expect(wrapper.find(".sub-menu").exists()).toBe(true);
+ expect(wrapper.find(".sub-menu").text()).toContain("Memory");
+
+ await wrapper.trigger("mouseleave");
+
+ expect(wrapper.find(".sub-menu").exists()).toBe(false);
+ });
+});
diff --git a/agdb_studio/src/components/base/menu/AgdbMenu.vue b/agdb_studio/src/components/base/menu/AgdbMenu.vue
new file mode 100644
index 000000000..6f778a9ba
--- /dev/null
+++ b/agdb_studio/src/components/base/menu/AgdbMenu.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
diff --git a/agdb_studio/src/components/base/table/AgdbCell.spec.ts b/agdb_studio/src/components/base/table/AgdbCell.spec.ts
new file mode 100644
index 000000000..e3049783e
--- /dev/null
+++ b/agdb_studio/src/components/base/table/AgdbCell.spec.ts
@@ -0,0 +1,151 @@
+import { describe, beforeEach, vi, it, expect } from "vitest";
+import { mount, shallowMount } from "@vue/test-utils";
+import AgdbCell from "./AgdbCell.vue";
+import {
+ INJECT_KEY_COLUMNS,
+ INJECT_KEY_ROW,
+} from "@/composables/table/constants";
+import { columnsMap } from "@/tests/tableMocks";
+
+describe("AgdbCell", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+ it("should render the cell", () => {
+ const wrapper = mount(AgdbCell, {
+ props: {
+ cellKey: "owner",
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_COLUMNS]: { value: columnsMap },
+ [INJECT_KEY_ROW]: {
+ value: {
+ role: "admin",
+ owner: "admin",
+ db: "test",
+ db_type: "memory",
+ size: 2656,
+ backup: 0,
+ },
+ },
+ },
+ },
+ });
+
+ expect(wrapper.find(".agdb-cell").exists()).toBe(true);
+ expect(wrapper.find(".agdb-cell").text()).toBe("admin");
+ });
+
+ it("should render the cell with a formatter", () => {
+ const wrapper = mount(AgdbCell, {
+ props: {
+ cellKey: "backup",
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_COLUMNS]: { value: columnsMap },
+ [INJECT_KEY_ROW]: {
+ value: {
+ role: "admin",
+ owner: "admin",
+ db: "test",
+ db_type: "memory",
+ size: 2656,
+ backup: 123456,
+ },
+ },
+ },
+ },
+ });
+
+ expect(wrapper.find(".agdb-cell").exists()).toBe(true);
+ expect(wrapper.find(".agdb-cell").text()).toBe("1");
+ });
+
+ it("should handle missing row data", () => {
+ const wrapper = mount(AgdbCell, {
+ props: {
+ cellKey: "backup",
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_COLUMNS]: { value: columnsMap },
+ [INJECT_KEY_ROW]: undefined,
+ },
+ },
+ });
+
+ expect(wrapper.find(".agdb-cell").exists()).toBe(true);
+ expect(wrapper.find(".agdb-cell").text()).toBe("0");
+ });
+
+ it("should display custom component", () => {
+ const columns = new Map();
+ columns.set("owner", {
+ key: "owner",
+ title: "Owner",
+ cellComponent: {
+ template: "Custom component
",
+ },
+ });
+ const wrapper = mount(AgdbCell, {
+ props: {
+ cellKey: "owner",
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_COLUMNS]: { value: columns },
+ [INJECT_KEY_ROW]: {
+ value: {
+ role: "admin",
+ owner: "admin",
+ db: "test",
+ db_type: "memory",
+ size: 2656,
+ backup: 0,
+ },
+ },
+ },
+ },
+ });
+
+ expect(wrapper.find(".agdb-cell").exists()).toBe(true);
+ expect(wrapper.find(".agdb-cell").text()).toBe("Custom component");
+ });
+ it("should display menu", async () => {
+ const columns = new Map();
+ columns.set("actions", {
+ key: "actions",
+ title: "Actions",
+ actions: [
+ {
+ key: "backup",
+ title: "Backup",
+ action: () => {},
+ },
+ ],
+ });
+ const wrapper = shallowMount(AgdbCell, {
+ props: {
+ cellKey: "actions",
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_COLUMNS]: { value: columns },
+ [INJECT_KEY_ROW]: {
+ value: {
+ role: "admin",
+ owner: "admin",
+ db: "test",
+ db_type: "memory",
+ size: 2656,
+ backup: 0,
+ },
+ },
+ },
+ },
+ });
+ expect(wrapper.html()).toContain("agdb-cell-menu-stub");
+ });
+});
diff --git a/agdb_studio/src/components/base/table/AgdbCell.vue b/agdb_studio/src/components/base/table/AgdbCell.vue
new file mode 100644
index 000000000..b9627d3cd
--- /dev/null
+++ b/agdb_studio/src/components/base/table/AgdbCell.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
diff --git a/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts b/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts
new file mode 100644
index 000000000..507f742d5
--- /dev/null
+++ b/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts
@@ -0,0 +1,114 @@
+import { mount } from "@vue/test-utils";
+import AgdbCellMenu from "./AgdbCellMenu.vue";
+import { describe, beforeEach, vi, it, expect } from "vitest";
+import { dbActions } from "@/composables/db/dbConfig";
+import { INJECT_KEY_ROW } from "@/composables/table/constants";
+const { fetchDatabases } = vi.hoisted(() => {
+ return {
+ fetchDatabases: vi.fn(),
+ };
+});
+
+vi.mock("@/composables/db/dbStore", () => {
+ return {
+ useDbStore: () => {
+ return {
+ fetchDatabases,
+ };
+ },
+ };
+});
+describe("AgdbCellMenu", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+ it("should open and close on click", async () => {
+ const wrapper = mount(AgdbCellMenu, {
+ props: {
+ actions: dbActions,
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_ROW]: {
+ value: {
+ role: "admin",
+ owner: "admin",
+ db: "test",
+ db_type: "memory",
+ size: 2656,
+ backup: 0,
+ },
+ },
+ },
+ },
+ });
+ const trigger = wrapper.find(".trigger");
+ expect(wrapper.find(".content").exists()).toBe(false);
+ trigger.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").isVisible()).toBe(true);
+ trigger.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").exists()).toBe(false);
+ });
+ it("should call action on click", async () => {
+ const wrapper = mount(AgdbCellMenu, {
+ props: {
+ actions: dbActions,
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_ROW]: {
+ value: {
+ role: "admin",
+ owner: "admin",
+ db: "test",
+ db_type: "memory",
+ size: 2656,
+ backup: 0,
+ },
+ },
+ },
+ },
+ });
+ const trigger = wrapper.find(".trigger");
+ expect(wrapper.find(".content").exists()).toBe(false);
+ trigger.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").isVisible()).toBe(true);
+ const action = wrapper.find(".menu-item[data-key=backup]");
+ await action.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").exists()).toBe(false);
+ });
+ it("should not close the dropdown if item has no action", async () => {
+ const wrapper = mount(AgdbCellMenu, {
+ props: {
+ actions: dbActions,
+ },
+ global: {
+ provide: {
+ [INJECT_KEY_ROW]: {
+ value: {
+ role: "admin",
+ owner: "admin",
+ db: "test",
+ db_type: "memory",
+ size: 2656,
+ backup: 0,
+ },
+ },
+ },
+ },
+ });
+ const trigger = wrapper.find(".trigger");
+ expect(wrapper.find(".content").exists()).toBe(false);
+ trigger.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").isVisible()).toBe(true);
+ const action = wrapper.find(".menu-item[data-key=convert]");
+ await action.trigger("click");
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(".content").exists()).toBe(true);
+ });
+});
diff --git a/agdb_studio/src/components/base/table/AgdbCellMenu.vue b/agdb_studio/src/components/base/table/AgdbCellMenu.vue
new file mode 100644
index 000000000..da478921a
--- /dev/null
+++ b/agdb_studio/src/components/base/table/AgdbCellMenu.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/agdb_studio/src/components/base/table/AgdbTable.spec.ts b/agdb_studio/src/components/base/table/AgdbTable.spec.ts
index c46b16341..49fd96c57 100644
--- a/agdb_studio/src/components/base/table/AgdbTable.spec.ts
+++ b/agdb_studio/src/components/base/table/AgdbTable.spec.ts
@@ -3,13 +3,14 @@ import AgdbTable from "./AgdbTable.vue";
import { addTable, clearTables } from "@/composables/table/tableConfig";
import { setTableData } from "@/composables/table/tableData";
import { TABLE_NAME, tableConfig, tableData } from "@/tests/tableMocks";
+import { describe, beforeEach, it, expect } from "vitest";
describe("AgdbTable", () => {
beforeEach(() => {
clearTables();
});
it("should render for correct data", () => {
- addTable({ name: TABLE_NAME, columns: tableConfig, uniqueKey: "name" });
+ addTable({ name: TABLE_NAME, columns: tableConfig });
setTableData(TABLE_NAME, tableData);
const wrapper = mount(AgdbTable, {
diff --git a/agdb_studio/src/components/base/table/AgdbTable.vue b/agdb_studio/src/components/base/table/AgdbTable.vue
index 23abdc5ad..aaf0d3b55 100644
--- a/agdb_studio/src/components/base/table/AgdbTable.vue
+++ b/agdb_studio/src/components/base/table/AgdbTable.vue
@@ -1,10 +1,14 @@
@@ -46,5 +53,6 @@ const columns = computed(() => {
gap: 1rem;
padding: 0.5rem;
border-bottom: 1px solid var(--color-border);
+ white-space: nowrap;
}
diff --git a/agdb_studio/src/components/base/table/TableHeader.spec.ts b/agdb_studio/src/components/base/table/AgdbTableHeader.spec.ts
similarity index 61%
rename from agdb_studio/src/components/base/table/TableHeader.spec.ts
rename to agdb_studio/src/components/base/table/AgdbTableHeader.spec.ts
index 4bd8da812..c022c9724 100644
--- a/agdb_studio/src/components/base/table/TableHeader.spec.ts
+++ b/agdb_studio/src/components/base/table/AgdbTableHeader.spec.ts
@@ -1,9 +1,10 @@
import { shallowMount } from "@vue/test-utils";
-import TableHeader from "./TableHeader.vue";
+import AgdbTableHeader from "./AgdbTableHeader.vue";
+import { describe, it, expect } from "vitest";
describe("TableHeader", () => {
it("should render", () => {
- const wrapper = shallowMount(TableHeader, {
+ const wrapper = shallowMount(AgdbTableHeader, {
props: {
tableKey: "table",
},
diff --git a/agdb_studio/src/components/base/table/TableHeader.vue b/agdb_studio/src/components/base/table/AgdbTableHeader.vue
similarity index 93%
rename from agdb_studio/src/components/base/table/TableHeader.vue
rename to agdb_studio/src/components/base/table/AgdbTableHeader.vue
index c1b8aaaff..1cb0ddb06 100644
--- a/agdb_studio/src/components/base/table/TableHeader.vue
+++ b/agdb_studio/src/components/base/table/AgdbTableHeader.vue
@@ -1,6 +1,6 @@
-
{{ getFromattedValue(cellKey) }}
+
diff --git a/agdb_studio/src/components/base/table/TableRow.spec.ts b/agdb_studio/src/components/base/table/TableRow.spec.ts
deleted file mode 100644
index 7c1eae1ce..000000000
--- a/agdb_studio/src/components/base/table/TableRow.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { shallowMount } from "@vue/test-utils";
-import TableRow from "./TableRow.vue";
-import { columnsMap } from "@/tests/tableMocks";
-
-describe("TableRow", () => {
- it("should render", () => {
- const wrapper = shallowMount(TableRow, {
- props: {
- columns: columnsMap,
- row: {
- role: "admin",
- name: "admin/app3",
- db_type: "file",
- size: 50,
- backup: 0,
- },
- },
- });
- expect(wrapper.text()).toContain("admin");
- });
-});
diff --git a/agdb_studio/src/components/db/DbAddForm.spec.ts b/agdb_studio/src/components/db/DbAddForm.spec.ts
index 5bab20ffb..e3e390452 100644
--- a/agdb_studio/src/components/db/DbAddForm.spec.ts
+++ b/agdb_studio/src/components/db/DbAddForm.spec.ts
@@ -1,5 +1,6 @@
import DbAddForm from "./DbAddForm.vue";
import { mount } from "@vue/test-utils";
+import { describe, beforeEach, vi, it, expect } from "vitest";
const { addDatabase } = vi.hoisted(() => {
return {
@@ -7,9 +8,9 @@ const { addDatabase } = vi.hoisted(() => {
};
});
-vi.mock("@/composables/stores/DbStore", () => {
+vi.mock("@/composables/db/dbStore", () => {
return {
- useDbList: () => {
+ useDbStore: () => {
return {
addDatabase,
};
@@ -22,6 +23,7 @@ describe("DbAddForm", () => {
vi.clearAllMocks();
});
it("should add a database when user submits", async () => {
+ addDatabase.mockResolvedValueOnce(true);
expect(addDatabase).not.toHaveBeenCalled();
const wrapper = mount(DbAddForm);
await wrapper.find("input").setValue("test_db");
@@ -31,6 +33,7 @@ describe("DbAddForm", () => {
expect(addDatabase).toHaveBeenCalledOnce();
});
it("should add a database when user clicks submit button", async () => {
+ addDatabase.mockResolvedValueOnce(true);
expect(addDatabase).not.toHaveBeenCalled();
const wrapper = mount(DbAddForm);
await wrapper.find("input").setValue("test_db");
diff --git a/agdb_studio/src/components/db/DbAddForm.vue b/agdb_studio/src/components/db/DbAddForm.vue
index dc8f92c5d..ec284a498 100644
--- a/agdb_studio/src/components/db/DbAddForm.vue
+++ b/agdb_studio/src/components/db/DbAddForm.vue
@@ -1,18 +1,32 @@
diff --git a/agdb_studio/src/components/db/DbList.spec.ts b/agdb_studio/src/components/db/DbList.spec.ts
deleted file mode 100644
index b13b300ac..000000000
--- a/agdb_studio/src/components/db/DbList.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import DbList from "./DbList.vue";
-import { mount, shallowMount } from "@vue/test-utils";
-
-const { databases, fetchDatabases } = vi.hoisted(() => {
- return {
- databases: [
- {
- name: "test_db",
- db_type: "memory",
- role: "admin",
- size: 2656,
- backup: 0,
- },
- {
- name: "test_db2",
- db_type: "memory",
- role: "admin",
- size: 2656,
- backup: 0,
- },
- ],
-
- fetchDatabases: vi.fn(),
- };
-});
-
-vi.mock("@/composables/stores/DbStore", () => {
- return {
- useDbList: () => {
- return {
- databases,
- fetchDatabases,
- };
- },
- };
-});
-
-describe("DbList", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
- it("should render databases when the page view loads", () => {
- const wrapper = shallowMount(DbList);
- expect(wrapper.html()).toContain("db-add-form-stub");
- expect(wrapper.html()).toContain("db-table-stub");
- });
- it("should fetch databases when the page view loads", () => {
- expect(fetchDatabases).not.toHaveBeenCalled();
- mount(DbList);
- expect(fetchDatabases).toHaveBeenCalledOnce();
- });
- it("should render a message when there are no databases", () => {
- databases.length = 0;
- const wrapper = mount(DbList);
- expect(wrapper.text()).toContain("No databases found");
- });
- it("should refresh databases when user clicks refresh button", async () => {
- expect(fetchDatabases).not.toHaveBeenCalled();
- const wrapper = mount(DbList);
- await wrapper.find("button").trigger("click");
- await wrapper.vm.$nextTick();
- expect(fetchDatabases).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/agdb_studio/src/components/db/DbList.vue b/agdb_studio/src/components/db/DbList.vue
deleted file mode 100644
index d09e05076..000000000
--- a/agdb_studio/src/components/db/DbList.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/agdb_studio/src/components/db/DbTable.spec.ts b/agdb_studio/src/components/db/DbTable.spec.ts
index 6dcd4ce53..2e34c2522 100644
--- a/agdb_studio/src/components/db/DbTable.spec.ts
+++ b/agdb_studio/src/components/db/DbTable.spec.ts
@@ -1,5 +1,6 @@
import { shallowMount } from "@vue/test-utils";
import DbTable from "./DbTable.vue";
+import { describe, it, expect } from "vitest";
describe("DbTable", () => {
it("should render", () => {
diff --git a/agdb_studio/src/components/db/DbTable.vue b/agdb_studio/src/components/db/DbTable.vue
index a90169113..3fb2efc2b 100644
--- a/agdb_studio/src/components/db/DbTable.vue
+++ b/agdb_studio/src/components/db/DbTable.vue
@@ -1,29 +1,18 @@
-
+
+
+
-
+
diff --git a/agdb_studio/vitest.config.mts b/agdb_studio/vitest.config.mts
index c62b3846b..24e1c8bb0 100644
--- a/agdb_studio/vitest.config.mts
+++ b/agdb_studio/vitest.config.mts
@@ -1,4 +1,3 @@
-// import { fileURLToPath } from "node:url";
import {
mergeConfig,
defineConfig,
@@ -15,7 +14,6 @@ export default mergeConfig(
environment: "jsdom",
exclude: [...configDefaults.exclude, "e2e/*"],
root: path.resolve(__dirname, "."),
- // root: fileURLToPath(new URL("./", import.meta.url)),
coverage: {
provider: "istanbul",
all: true,