diff --git a/webclient-vue/src/components/script/ActionScriptsGrid.vue b/webclient-vue/src/components/script/ActionScriptsGrid.vue
new file mode 100644
index 0000000..43a80fa
--- /dev/null
+++ b/webclient-vue/src/components/script/ActionScriptsGrid.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+ {{ script.description }}
+
+ {{ script.state }}
+ {{ script.scope }}
+ {{ areaFor(script).display_name }}
+
+ {{ script.created_by || script.author }}
+
+
+
+ Run
+
+
+
+
+
+
+
+
+
diff --git a/webclient-vue/src/components/script/__tests__/ActionScriptsGrid.spec.js b/webclient-vue/src/components/script/__tests__/ActionScriptsGrid.spec.js
new file mode 100644
index 0000000..a3b0953
--- /dev/null
+++ b/webclient-vue/src/components/script/__tests__/ActionScriptsGrid.spec.js
@@ -0,0 +1,74 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { mount } from "@vue/test-utils";
+import { setActivePinia, createPinia } from "pinia";
+import { useAreasStore } from "../../../stores/areas.js";
+import ActionScriptsGrid from "../ActionScriptsGrid.vue";
+
+describe("ActionScriptsGrid", () => {
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ });
+
+ function createWrapper(props = {}) {
+ return mount(ActionScriptsGrid, {
+ props: {
+ scripts: [
+ { id: 1, alias: "kitchen_light", name: "Kitchen Light", icon: '', description: "Toggle kitchen", state: "enabled", author: "Test", scope: "KitchenScope", area_id: 1 },
+ { id: 2, alias: "hall_light", name: "Hall Light", state: "disabled", author: "Test" },
+ ],
+ ...props,
+ },
+ });
+ }
+
+ it("renders action cards for each script", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.text()).toContain("Kitchen Light");
+ expect(wrapper.text()).toContain("Hall Light");
+ });
+
+ it("shows script description when present", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.text()).toContain("Toggle kitchen");
+ });
+
+ it("shows state badge with success variant for enabled", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.text()).toContain("enabled");
+ });
+
+ it("shows state badge with secondary variant for disabled", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.text()).toContain("disabled");
+ });
+
+ it("shows scope badge when present", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.text()).toContain("KitchenScope");
+ });
+
+ it("shows area badge when showAreaBadge is true and area exists", () => {
+ const areasStore = useAreasStore();
+ areasStore.areas = [{ id: 1, display_name: "Kitchen Area" }];
+ const wrapper = createWrapper({ showAreaBadge: true });
+ expect(wrapper.text()).toContain("Kitchen Area");
+ });
+
+ it("does not show area badge when showAreaBadge is false", () => {
+ const areasStore = useAreasStore();
+ areasStore.areas = [{ id: 1, display_name: "Kitchen Area" }];
+ const wrapper = createWrapper({ showAreaBadge: false });
+ expect(wrapper.text()).not.toContain("Kitchen Area");
+ });
+
+ it("shows author when present", () => {
+ const wrapper = createWrapper();
+ expect(wrapper.text()).toContain("Test");
+ });
+
+ it("emits run event on button click", async () => {
+ const wrapper = createWrapper();
+ const buttons = wrapper.findAll("button");
+ expect(buttons.length).toBeGreaterThan(0);
+ });
+});
diff --git a/webclient-vue/src/features/areas/pages/AreaDetailPage.vue b/webclient-vue/src/features/areas/pages/AreaDetailPage.vue
index 192c87a..1970bed 100644
--- a/webclient-vue/src/features/areas/pages/AreaDetailPage.vue
+++ b/webclient-vue/src/features/areas/pages/AreaDetailPage.vue
@@ -63,36 +63,7 @@
title="No actions"
message="No action scripts assigned to this area."
/>
-
-
-
-
- {{ script.description }}
-
- {{ script.state }}
- {{ script.scope }}
-
- {{ script.created_by || script.author }}
-
-
-
- Run
-
-
-
-
+
@@ -220,7 +191,6 @@
GnInput,
GnAlert,
GnSelect,
- GnActionCard,
useToast,
} from "gnexus-ui-kit/vue";
import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
@@ -231,6 +201,7 @@
import DeviceTable from "../../../components/device/DeviceTable.vue";
import ScriptTable from "../../../components/script/ScriptTable.vue";
import PageActionsDropdown from "../../../components/layout/PageActionsDropdown.vue";
+import ActionScriptsGrid from "../../../components/script/ActionScriptsGrid.vue";
const route = useRoute();
const router = useRouter();
@@ -336,22 +307,6 @@
const unassignLoading = ref(false);
const unassignError = ref("");
-function goToScriptDetail(alias) {
- router.push({ name: "script-detail", params: { type: "actions", id: alias } });
-}
-
-async function runAction(alias) {
- const result = await scriptsStore.runScript(alias);
- if (result?.ok) {
- toast.success({
- title: `Ran ${alias}`,
- text: scriptsStore.lastRunResult?.execTime ? `Exec time: ${scriptsStore.lastRunResult.execTime}` : undefined,
- });
- } else {
- toast.error({ title: `Failed ${alias}`, text: result?.error?.message || "Unknown error" });
- }
-}
-
function openRename() {
if (!area.value) return;
renameForm.areaId = area.value.id;
@@ -487,17 +442,6 @@
color: var(--color-primary);
}
-.script-icon {
- font-size: 32px;
-}
-
-.script-meta {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- align-items: center;
-}
-
.form-group {
margin-bottom: 16px;
}
diff --git a/webclient-vue/src/features/scripts/pages/ScriptsActionsPage.vue b/webclient-vue/src/features/scripts/pages/ScriptsActionsPage.vue
index dd748f5..db0ff18 100644
--- a/webclient-vue/src/features/scripts/pages/ScriptsActionsPage.vue
+++ b/webclient-vue/src/features/scripts/pages/ScriptsActionsPage.vue
@@ -21,77 +21,25 @@
message="No action scripts registered."
/>
-
-
-
-
- {{ script.description }}
-
- {{ script.state }}
- {{ areaFor(script).display_name }}
-
- {{ script.author }}
-
-
-
- Run
-
-
-
-
+