Skip to content

Commit 4397f51

Browse files
committed
feat(elements-vue): enhance Vue components to match React implementation
- Add new UI components: Icon, ProviderLogo, DefaultToast, DefaultSsoButtonContainer - Add toast notification system with vue-sonner integration - Fix recovery codes grid display (5-column layout) - Fix provider logos in OIDC settings (show actual logos instead of letters) - Remove wrapper div from OrySettingsCard (use Vue 3 fragments) - Consolidate icon system into unified Icon.vue component - Add provider logos support matching React implementation - Simplify code and remove duplicate utilities - Update dependencies for vue-sonner support
1 parent 5cfb80b commit 4397f51

68 files changed

Lines changed: 2368 additions & 1155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 541 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"@storybook/node-logger": "8.6.14",
4949
"@storybook/react": "8.6.14",
5050
"@storybook/react-vite": "8.6.14",
51+
"@storybook/vue3": "8.6.14",
52+
"@storybook/vue3-vite": "8.6.14",
5153
"@storybook/test-runner": "^0.23.0",
5254
"@storybook/testing-library": "^0.2.2",
5355
"@storybook/types": "8.6.14",

packages/elements-vue/package.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"type-check": "vue-tsc --noEmit",
77
"test": "vitest run",
88
"test:watch": "vitest",
9-
"test:coverage": "vitest run --coverage"
9+
"test:coverage": "vitest run --coverage",
10+
"storybook": "storybook dev -p 6007",
11+
"storybook:build": "storybook build"
1012
},
1113
"exports": {
1214
".": {
@@ -49,24 +51,34 @@
4951
"class-variance-authority": "0.7.1",
5052
"clsx": "2.1.1",
5153
"tailwind-merge": "3.3.0",
52-
"vue-i18n": "^11.0.0"
54+
"vue-i18n": "^11.0.0",
55+
"vue-sonner": "^2.0.0"
5356
},
5457
"peerDependencies": {
5558
"vue": "^3.4.0"
5659
},
5760
"devDependencies": {
61+
"@storybook/addon-a11y": "8.6.14",
62+
"@storybook/addon-essentials": "8.6.14",
63+
"@storybook/addon-interactions": "8.6.14",
64+
"@storybook/vue3": "8.6.14",
65+
"@storybook/vue3-vite": "8.6.14",
66+
"@tailwindcss/vite": "^4.0.0",
5867
"@testing-library/vue": "^8.1.0",
5968
"@vitejs/plugin-vue": "^5.1.0",
6069
"@vue/test-utils": "^2.4.0",
6170
"autoprefixer": "10.4.21",
6271
"esbuild-plugin-vue-next": "^0.1.4",
6372
"happy-dom": "^15.0.0",
6473
"postcss": "8.4.47",
74+
"storybook": "8.6.14",
75+
"storybook-addon-mock": "5.0.0",
6576
"tailwindcss": "^3.4.0",
6677
"tsup": "8.4.0",
6778
"typescript": "^5.5.0",
6879
"unplugin-vue": "^7.1.0",
6980
"vite": "^5.4.0",
81+
"vite-plugin-require": "1.2.14",
7082
"vitest": "^2.0.0",
7183
"vue": "^3.5.0",
7284
"vue-tsc": "^2.0.0"

packages/elements-vue/project.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@
7474
]
7575
},
7676
"dependsOn": ["build"]
77+
},
78+
"storybook": {
79+
"executor": "@nx/storybook:storybook",
80+
"options": {
81+
"port": 6007,
82+
"configDir": "packages/elements-vue/.storybook"
83+
}
84+
},
85+
"storybook:build": {
86+
"executor": "@nx/storybook:build",
87+
"outputs": ["{options.outputDir}"],
88+
"options": {
89+
"outputDir": "dist/storybook/elements-vue",
90+
"configDir": "packages/elements-vue/.storybook"
91+
}
7792
}
7893
}
7994
}

packages/elements-vue/src/components/card/two-step/ProvideIdentifierForm.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import OryCardHeader from "../OryCardHeader.vue"
1515
import OryCardContent from "../OryCardContent.vue"
1616
import OryCardFooter from "../OryCardFooter.vue"
1717
import OryForm from "../../form/OryForm.vue"
18+
import OryFormSsoForm from "../../form/OryFormSsoForm.vue"
1819
import OryMessages from "../../form/OryMessages.vue"
1920
import OryNode from "../../form/nodes/OryNode.vue"
2021
import { handleAfterFormSubmit } from "./utils"
@@ -50,6 +51,7 @@ const showSsoDivider = computed(
5051
<OryCardHeader />
5152
<OryCardContent>
5253
<OryMessages :messages="flowContainer.flow.ui.messages" />
54+
<OryFormSsoForm />
5355
<OryForm :on-after-submit="handleAfterFormSubmit(dispatchFormState)">
5456
<component :is="Form.Group">
5557
<component :is="Card.Divider" v-if="showSsoDivider" />

packages/elements-vue/src/components/card/two-step/SelectMethodForm.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import OryCardHeader from "../OryCardHeader.vue"
2626
import OryCardContent from "../OryCardContent.vue"
2727
import OryCardFooter from "../OryCardFooter.vue"
2828
import OryForm from "../../form/OryForm.vue"
29+
import OryFormSsoForm from "../../form/OryFormSsoForm.vue"
2930
import OryMessages from "../../form/OryMessages.vue"
3031
import OryNode from "../../form/nodes/OryNode.vue"
3132
import { handleAfterFormSubmit, toAuthMethodPickerOptions } from "./utils"
@@ -109,7 +110,7 @@ function handleMethodClick(group: string) {
109110
<OryCardHeader />
110111
<OryCardContent>
111112
<OryMessages :messages="flowContainer.flow.ui.messages" />
112-
113+
<OryFormSsoForm />
113114
<OryForm
114115
v-if="hasAuthMethods"
115116
:on-after-submit="handleAfterFormSubmit(dispatchFormState)"

packages/elements-vue/src/components/card/two-step/utils.ts

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// Copyright © 2026 Ory Corp
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { FlowType, UiNode, UiNodeGroupEnum } from "@ory/client-fetch"
5-
import type { OryFlowContainer } from "../../../util/flowContainer"
4+
import { UiNode, UiNodeGroupEnum } from "@ory/client-fetch"
65
import { isUiNodeGroupEnum, type GroupedNodes } from "../../../util/ui"
76
import type { FormStateAction } from "../../../composables/useOryFlow"
87

@@ -44,45 +43,6 @@ export function toAuthMethodPickerOptions(
4443
)
4544
}
4645

47-
function isScreenSelectionNode(node: UiNode) {
48-
if (
49-
"name" in node.attributes &&
50-
node.attributes.name === "screen" &&
51-
"value" in node.attributes &&
52-
node.attributes.value === "previous"
53-
) {
54-
return true
55-
}
56-
if (
57-
node.group === UiNodeGroupEnum.IdentifierFirst &&
58-
"name" in node.attributes &&
59-
node.attributes.name === "identifier" &&
60-
node.attributes.type === "hidden"
61-
) {
62-
return true
63-
}
64-
return false
65-
}
66-
67-
/**
68-
* Check if the flow is in choosing method state
69-
*/
70-
export function isChoosingMethod(flow: OryFlowContainer): boolean {
71-
if (flow.flowType === FlowType.Login) {
72-
const loginFlow = flow.flow as { requested_aal?: string; refresh?: boolean }
73-
if (loginFlow.requested_aal === "aal2") {
74-
return true
75-
}
76-
if (
77-
loginFlow.refresh &&
78-
!flow.flow.ui.nodes.some((n) => n.group === "code")
79-
) {
80-
return true
81-
}
82-
}
83-
return flow.flow.ui.nodes.some(isScreenSelectionNode)
84-
}
85-
8646
/**
8747
* Get the final nodes to display based on the selected group
8848
*/

packages/elements-vue/src/components/form/OryForm.vue

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,64 @@
22
<!-- SPDX-License-Identifier: Apache-2.0 -->
33

44
<script setup lang="ts">
5-
import { watch } from "vue"
6-
import { FlowType } from "@ory/client-fetch"
5+
import { computed, watch } from "vue"
6+
import {
7+
FlowType,
8+
isUiNodeInputAttributes,
9+
isUiNodeAnchorAttributes,
10+
isUiNodeImageAttributes,
11+
isUiNodeScriptAttributes,
12+
} from "@ory/client-fetch"
713
import { useComponents } from "../../composables/useComponents"
814
import { useOryForm } from "../../composables/useOryForm"
915
import { provideOryForm } from "../../composables/useOryFormContext"
1016
import { useOryFlow } from "../../composables/useOryFlow"
17+
import { useOryIntl } from "../../composables/useOryIntl"
1118
1219
const props = defineProps<{
1320
onAfterSubmit?: (method: string | number | boolean | undefined) => void
1421
}>()
1522
1623
const components = useComponents()
17-
const { formState, flowType } = useOryFlow()
24+
const { flowContainer, formState, flowType } = useOryFlow()
25+
const intl = useOryIntl()
1826
const formContext = useOryForm({
1927
onAfterSubmit: props.onAfterSubmit,
2028
})
2129
const { handleSubmit, action, method, setValue } = formContext
2230
2331
provideOryForm(formContext)
2432
33+
const hasMethods = computed(() =>
34+
flowContainer.value.flow.ui.nodes.some((node) => {
35+
if (isUiNodeInputAttributes(node.attributes)) {
36+
if (node.attributes.type === "hidden") {
37+
return false
38+
}
39+
return node.attributes.name !== "csrf_token"
40+
} else if (isUiNodeAnchorAttributes(node.attributes)) {
41+
return true
42+
} else if (isUiNodeImageAttributes(node.attributes)) {
43+
return true
44+
} else if (isUiNodeScriptAttributes(node.attributes)) {
45+
return true
46+
}
47+
return false
48+
}),
49+
)
50+
51+
const noMethodsMessage = computed(() => {
52+
const translationKey = "identities.messages.5000002"
53+
const defaultMessage =
54+
"No authentication methods are available for this request. Please contact the site or app owner."
55+
const translated = intl.t(translationKey)
56+
return {
57+
id: 5000002,
58+
text: translated === translationKey ? defaultMessage : translated,
59+
type: "error" as const,
60+
}
61+
})
62+
2563
/**
2664
* Method field enforcement for code flows.
2765
* Sometimes the method input node is missing, so we force set it here.
@@ -43,7 +81,16 @@ watch(
4381
</script>
4482

4583
<template>
84+
<template v-if="!hasMethods">
85+
<component :is="components.Message.Root">
86+
<component
87+
:is="components.Message.Content"
88+
:message="noMethodsMessage"
89+
/>
90+
</component>
91+
</template>
4692
<component
93+
v-else
4794
:is="components.Form.Root"
4895
:action="action"
4996
:method="method"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!-- Copyright © 2026 Ory Corp -->
2+
<!-- SPDX-License-Identifier: Apache-2.0 -->
3+
4+
<script setup lang="ts">
5+
import { computed } from "vue"
6+
import { UiNodeGroupEnum, getNodeId } from "@ory/client-fetch"
7+
import { useComponents } from "../../composables/useComponents"
8+
import { useOryForm } from "../../composables/useOryForm"
9+
import { provideOryForm } from "../../composables/useOryFormContext"
10+
import { useOryFlow } from "../../composables/useOryFlow"
11+
import { isNodeVisible } from "../../util/ui"
12+
import OryNode from "./nodes/OryNode.vue"
13+
14+
const { Form } = useComponents()
15+
const { flowContainer } = useOryFlow()
16+
17+
const formContext = useOryForm({})
18+
const { handleSubmit, action, method } = formContext
19+
20+
provideOryForm(formContext)
21+
22+
const ssoNodes = computed(() =>
23+
flowContainer.value.flow.ui.nodes.filter(
24+
(node) =>
25+
(node.group === UiNodeGroupEnum.Oidc ||
26+
node.group === UiNodeGroupEnum.Saml) &&
27+
isNodeVisible(node),
28+
),
29+
)
30+
</script>
31+
32+
<template>
33+
<component
34+
v-if="ssoNodes.length > 0"
35+
:is="Form.Root"
36+
:action="action"
37+
:method="method"
38+
data-testid="ory/form/methods/oidc-saml"
39+
@submit="handleSubmit"
40+
>
41+
<component :is="Form.SsoRoot" :nodes="ssoNodes">
42+
<OryNode v-for="node in ssoNodes" :key="getNodeId(node)" :node="node" />
43+
</component>
44+
</component>
45+
</template>

packages/elements-vue/src/components/form/OryMessages.vue

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
<!-- Copyright © 2026 Ory Corp -->
22
<!-- SPDX-License-Identifier: Apache-2.0 -->
33

4+
<script lang="ts">
5+
const DEFAULT_HIDDEN_MESSAGE_IDS = [
6+
1040009, 1060003, 1080003, 1010004, 1010014, 1040005, 1010016, 1010003,
7+
1060004, 1060005, 1060006,
8+
]
9+
</script>
10+
411
<script setup lang="ts">
12+
import { computed } from "vue"
513
import type { UiText } from "@ory/client-fetch"
614
import { useComponents } from "../../composables/useComponents"
715
8-
withDefaults(
16+
const props = withDefaults(
917
defineProps<{
1018
messages?: UiText[]
19+
hiddenMessageIds?: number[]
1120
}>(),
1221
{
1322
messages: () => [],
23+
hiddenMessageIds: () => DEFAULT_HIDDEN_MESSAGE_IDS,
1424
},
1525
)
1626
1727
const components = useComponents()
28+
29+
const filteredMessages = computed(() =>
30+
props.messages?.filter((m) => !props.hiddenMessageIds.includes(m.id)) ?? [],
31+
)
1832
</script>
1933

2034
<template>
2135
<component
2236
:is="components.Message.Root"
23-
v-if="messages && messages.length > 0"
37+
v-if="filteredMessages.length > 0"
2438
>
2539
<component
2640
:is="components.Message.Content"
27-
v-for="message in messages"
41+
v-for="message in filteredMessages"
2842
:key="message.id"
2943
:message="message"
3044
/>

0 commit comments

Comments
 (0)