diff --git a/apps/client-web/src/components/client/clientCard/clientCard.md b/apps/client-web/src/components/client/clientCard/clientCard.md index afe539e..0e78c3d 100644 --- a/apps/client-web/src/components/client/clientCard/clientCard.md +++ b/apps/client-web/src/components/client/clientCard/clientCard.md @@ -2,11 +2,11 @@ ## Rationale -Display and edit a single client in the client list. +Display and edit a registered Client in the deployment Client list. ## Goals -Allow users to view client details and edit client name and URL. +Allow users to view Client details and explicitly save its name, management URL, or configuration. ## Key Concepts @@ -14,11 +14,11 @@ Client management, health status. ## Specification -Shows client name, ID, URL, and status. Has edit mode with inputs for name and URL, save/cancel buttons. +Shows Client name, ID, management URL, and health status. Name and URL changes use an explicit save action. Configuration changes use a JSON dialog. ## Implementation -Uses reactive editing state, emits updated on save. +Updates only an existing Client and emits `updated` after a successful save. ### Props diff --git a/apps/client-web/src/components/client/clientCard/clientCard.scss b/apps/client-web/src/components/client/clientCard/clientCard.scss index 4bb3995..1062231 100644 --- a/apps/client-web/src/components/client/clientCard/clientCard.scss +++ b/apps/client-web/src/components/client/clientCard/clientCard.scss @@ -10,6 +10,8 @@ display: flex; flex-direction: column; gap: sys-var(space, xs); + flex: 1; + min-width: 0; } &__item-name { diff --git a/apps/client-web/src/components/client/clientCard/clientCard.spec.ts b/apps/client-web/src/components/client/clientCard/clientCard.spec.ts new file mode 100644 index 0000000..2792aa4 --- /dev/null +++ b/apps/client-web/src/components/client/clientCard/clientCard.spec.ts @@ -0,0 +1,87 @@ +import { defineComponent } from 'vue' +import { flushPromises, mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { Client } from '@inkcre/core' +import ClientCard from './clientCard.vue' + +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) + +const stubs = { + InkInput: defineComponent({ + props: ['modelValue'], + emits: ['update:modelValue'], + template: + '', + }), + InkButton: defineComponent({ + props: ['text', 'loading'], + emits: ['click'], + template: '', + }), + InkDialog: defineComponent({ template: '
' }), + InkJsonEditor: true, +} + +describe('ClientCard', () => { + beforeEach(() => { + vi.stubGlobal('alert', vi.fn()) + vi.spyOn(console, 'error').mockImplementation(() => undefined) + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + it('explicitly updates an existing Client without an upsert lifecycle', async () => { + const single = vi.fn().mockResolvedValue({ data: {}, error: null, status: 200 }) + const select = vi.fn(() => ({ single })) + const eq = vi.fn(() => ({ select })) + const update = vi.spyOn(Client.dbApi, 'update').mockReturnValue({ eq } as never) + const client = Client.parse({ + id: '00000000-0000-4000-8000-000000000003', + name: 'Old name', + rest_api_url: 'https://old.example.test/', + }) + const wrapper = mount(ClientCard, { + props: { client, status: 'unknown' }, + global: { stubs }, + }) + + const inputs = wrapper.findAll('input') + await inputs[0].setValue('Renamed Client') + await inputs[1].setValue('https://new.example.test/') + const save = wrapper.findAll('button').find((button) => button.text() === 'settings.saveConfig') + await save?.trigger('click') + await flushPromises() + + expect(update).toHaveBeenCalledWith({ + name: 'Renamed Client', + rest_api_url: 'https://new.example.test/', + }) + expect(eq).toHaveBeenCalledWith('id', client.id) + expect(single).toHaveBeenCalledOnce() + expect(wrapper.emitted('updated')).toHaveLength(1) + }) + + it('rejects an invalid management URL before writing the Client row', async () => { + const update = vi.spyOn(Client.dbApi, 'update') + const client = Client.parse({ + id: '00000000-0000-4000-8000-000000000003', + name: 'Core', + rest_api_url: 'https://core.example.test/', + }) + const wrapper = mount(ClientCard, { + props: { client, status: 'unknown' }, + global: { stubs }, + }) + + await wrapper.findAll('input')[1].setValue('not a URL') + const save = wrapper.findAll('button').find((button) => button.text() === 'settings.saveConfig') + await save?.trigger('click') + await flushPromises() + + expect(update).not.toHaveBeenCalled() + expect(alert).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/client-web/src/components/client/clientCard/clientCard.vue b/apps/client-web/src/components/client/clientCard/clientCard.vue index f4b001a..b328731 100644 --- a/apps/client-web/src/components/client/clientCard/clientCard.vue +++ b/apps/client-web/src/components/client/clientCard/clientCard.vue @@ -1,8 +1,8 @@