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: '
{{ t('settings.allClientsNotice') }}
+Twitter setup
', +}) +const host = { + getSetupContribution: vi.fn((): { component: typeof SetupComponent } | null => ({ + component: SetupComponent, + })), + changeVersion: vi.fn(), + updateConfig: vi.fn(), + uninstall: vi.fn(), +} + +vi.mock('@/core', () => ({ getExtensionHost: () => host })) +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) + +const extension = { + name: 'inkcre/twitter', + version: '0.2.0', + enabled: ['00000000-0000-4000-8000-000000000001'], + nickname: 'Twitter', + config: {}, + config_schema: null, +} + +const stubs = { + InkButton: defineComponent({ + props: ['text'], + emits: ['click'], + template: '', + }), + InkSwitch: defineComponent({ + props: ['modelValue'], + emits: ['update:modelValue'], + template: '', + }), + InkDialog: defineComponent({ template: '+ {{ t('extension.desiredStateOnly') }} +
++ {{ t('extension.clientListUnavailable', { error: clientError }) }} +
+{{ error }}
@@ -57,8 +154,9 @@ const updExtension = (updatedExtension: InstalledExtension) => { v-else :key="extension.name" :extension="extension" - :enabled="isEnabledForCurrentPeer(extension)" - @changed="refreshExtensions" + :enabled="isEnabledForSelectedClient(extension)" + :controls-current-web-runtime="selectedControlMode === 'current-runtime'" + :set-enabled="(enabled) => setEnabledForSelectedClient(extension, enabled)" @updated="updExtension" @uninstalled="refreshExtensions" /> diff --git a/apps/client-web/src/views/settings/settings.scss b/apps/client-web/src/views/settings/settings.scss index d8f83b9..9b5b610 100644 --- a/apps/client-web/src/views/settings/settings.scss +++ b/apps/client-web/src/views/settings/settings.scss @@ -3,7 +3,7 @@ padding: sys-var('space', 'lg'); display: flex; flex-direction: column; - gap: sys-var('space', 'lg'); + gap: sys-var('space', 'xl'); overflow-y: auto; &__title { @@ -12,12 +12,35 @@ color: sys-var('color', 'text', 'base'); } + &__scope { + display: flex; + flex-direction: column; + gap: sys-var('space', 'lg'); + padding: sys-var('space', 'lg'); + background: sys-var('color', 'surface', 'base'); + border: 1px solid sys-var('color', 'border', 'base'); + } + + &__scope-title { + @include apply-font('title', 'lg'); + margin: 0; + padding-bottom: sys-var('space', 'md'); + color: sys-var('color', 'text', 'base'); + border-bottom: 1px solid sys-var('color', 'border', 'base'); + } + &__form { display: flex; flex-direction: column; gap: sys-var('space', 'md'); } + &__section-title { + @include apply-font('title', 'md'); + margin: sys-var('space', 'sm') 0 0; + color: sys-var('color', 'text', 'base'); + } + &__notice, &__export-note { margin: 0; @@ -25,6 +48,7 @@ } &__secret, + &__identity, &__locale { display: flex; flex-direction: column; @@ -43,6 +67,14 @@ } } + &__identity code { + padding: sys-var('space', 'sm'); + overflow-wrap: anywhere; + color: sys-var('color', 'text', 'base'); + background: sys-var('color', 'surface', 'subtle'); + border: 1px solid sys-var('color', 'border', 'base'); + } + &__actions { display: flex; gap: sys-var('space', 'md'); diff --git a/apps/client-web/src/views/settings/settings.spec.ts b/apps/client-web/src/views/settings/settings.spec.ts new file mode 100644 index 0000000..edeaa37 --- /dev/null +++ b/apps/client-web/src/views/settings/settings.spec.ts @@ -0,0 +1,108 @@ +import { defineComponent, onMounted } from 'vue' +import { flushPromises, mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ClientConfigSchema, configStore, MetaConfigSchema } from '@inkcre/core' +import Settings from './settings.vue' + +const clientListState = vi.hoisted(() => ({ mounts: 0 })) + +vi.mock('@/components/client/clientList/clientList.vue', () => ({ + default: defineComponent({ + name: 'ClientList', + setup() { + onMounted(() => { + clientListState.mounts += 1 + }) + }, + template: '', + }), +})) + +vi.mock('vue-i18n', async (importOriginal) => ({ + ...(await importOriginal{{ error }}
+ ++ The setup commands run on a Core Peer, but configure Twitter for the whole deployment. +
+ +No reachable Core Peer exposes the installed Twitter Extension.
+
+ Register your own X OAuth 2.0 application and use this callback URL:
+ {{ status?.callback_url }}
+
+ Waiting for the standalone Core callback… +
+Connected as @{{ status.handle }}.
+Change an existing Source schedule from the Sources page.
+