adata-ui 2.1.41-beta.12 → 2.1.41-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
3
3
  import { writeAccessTokenCookie } from '#adata-ui/composables/useAccessTokenCookie'
4
+
5
+ const emit = defineEmits<{
6
+ (e: 'closed', value: boolean): void
7
+ }>()
4
8
  // Каждое из этих окон рендерится только под своим v-if, но статический import
5
9
  // всё равно тащил все десять (плюс zod, который они используют для валидации)
6
10
  // в стартовый чанк каждого приложения-потребителя. defineAsyncComponent грузит
@@ -16,10 +20,6 @@ const IdRegistrationModal = defineAsyncComponent(() => import('#adata-ui/compone
16
20
  const IdResetPasswordOtpModal = defineAsyncComponent(() => import('#adata-ui/components/modals/id/IdResetPasswordOtpModal.vue'))
17
21
  const IdTwoFactorModal = defineAsyncComponent(() => import('#adata-ui/components/modals/id/IdTwoFactorModal.vue'))
18
22
 
19
- const emit = defineEmits<{
20
- (e: 'closed', value: boolean): void
21
- }>()
22
-
23
23
  const {
24
24
  loginModal,
25
25
  registrationModal,
@@ -30,7 +30,7 @@ const {
30
30
  passwordSuccessfulModal,
31
31
  confirmSuccessfulModal,
32
32
  twoFactorModal,
33
- autoLogoutModal
33
+ autoLogoutModal,
34
34
  } = useIdModals()
35
35
 
36
36
  const { locale } = useI18n()
@@ -86,42 +86,84 @@ watch(isAuthenticated, (val) => {
86
86
  }
87
87
  })
88
88
 
89
- const autoLoginClose = () => {
89
+ function autoLoginClose() {
90
90
  emit('closed', true)
91
91
  autoLogoutModal.value = false
92
92
  }
93
93
  </script>
94
94
 
95
95
  <template>
96
- <a-modal v-model="loginModal" data-test-id="login-form-modal">
96
+ <lazy-a-modal-v2
97
+ v-model="loginModal"
98
+ width="424px"
99
+ hide-header
100
+ data-test-id="login-form-modal"
101
+ >
97
102
  <id-login-modal v-if="loginModal" />
98
- </a-modal>
99
- <a-modal v-model="twoFactorModal">
103
+ </lazy-a-modal-v2>
104
+ <lazy-a-modal-v2
105
+ v-model="twoFactorModal"
106
+ width="424px"
107
+ hide-header
108
+ >
100
109
  <id-two-factor-modal v-if="twoFactorModal" />
101
- </a-modal>
110
+ </lazy-a-modal-v2>
102
111
 
103
- <a-modal v-model="registrationModal">
112
+ <lazy-a-modal-v2
113
+ v-model="registrationModal"
114
+ width="424px"
115
+ hide-header
116
+ >
104
117
  <id-registration-modal v-if="registrationModal" />
105
- </a-modal>
106
- <a-modal v-model="confirmAccountOtpModal" prevent-close>
118
+ </lazy-a-modal-v2>
119
+ <lazy-a-modal-v2
120
+ v-model="confirmAccountOtpModal"
121
+ width="424px"
122
+ hide-header
123
+ persistent
124
+ >
107
125
  <id-confirm-account-otp-modal v-if="confirmAccountOtpModal" />
108
- </a-modal>
109
- <a-modal v-model="confirmSuccessfulModal" prevent-close>
126
+ </lazy-a-modal-v2>
127
+ <lazy-a-modal-v2
128
+ v-model="confirmSuccessfulModal"
129
+ width="424px"
130
+ hide-header
131
+ persistent
132
+ >
110
133
  <id-confirm-successful-modal v-if="confirmSuccessfulModal" />
111
- </a-modal>
134
+ </lazy-a-modal-v2>
112
135
 
113
- <a-modal v-model="recoveryModal">
136
+ <lazy-a-modal-v2
137
+ v-model="recoveryModal"
138
+ width="424px"
139
+ hide-header
140
+ >
114
141
  <id-recovery-modal v-if="recoveryModal" />
115
- </a-modal>
116
- <a-modal v-model="resetPasswordOtpModal" prevent-close>
142
+ </lazy-a-modal-v2>
143
+ <lazy-a-modal-v2
144
+ v-model="resetPasswordOtpModal"
145
+ width="424px"
146
+ hide-header
147
+ persistent
148
+ >
117
149
  <id-reset-password-otp-modal v-if="resetPasswordOtpModal" />
118
- </a-modal>
119
- <a-modal v-model="newPasswordModal" prevent-close>
150
+ </lazy-a-modal-v2>
151
+ <lazy-a-modal-v2
152
+ v-model="newPasswordModal"
153
+ width="424px"
154
+ hide-header
155
+ persistent
156
+ >
120
157
  <id-new-password-modal v-if="newPasswordModal" />
121
- </a-modal>
122
- <a-modal v-model="passwordSuccessfulModal" prevent-close>
158
+ </lazy-a-modal-v2>
159
+ <lazy-a-modal-v2
160
+ v-model="passwordSuccessfulModal"
161
+ width="424px"
162
+ hide-header
163
+ persistent
164
+ >
123
165
  <id-password-successful-modal v-if="passwordSuccessfulModal" />
124
- </a-modal>
166
+ </lazy-a-modal-v2>
125
167
 
126
168
  <id-auto-logout-modal v-if="autoLogoutModal" @closed="autoLoginClose" />
127
169
  </template>
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
3
+ import AIconLock from '#adata-ui/icons/lock.vue'
4
+ import { apiErrorMessage } from '#adata-ui/utils/apiErrorMessage'
5
+ import { PASSWORD_MIN_LENGTH, PASSWORD_RULES } from '#adata-ui/utils/passwordRules'
3
6
  import * as z from 'zod'
4
7
 
5
8
  const { $toast } = useNuxtApp()
@@ -15,17 +18,21 @@ const form = reactive({
15
18
  })
16
19
  const loading = ref(false)
17
20
 
21
+ /* Правила берутся из PASSWORD_RULES — того же источника, что и чеклист
22
+ <a-password-requirements>, поэтому валидация и подсказки не расходятся. */
23
+ function passwordField() {
24
+ return PASSWORD_RULES.reduce(
25
+ (schema: z.ZodType<string>, rule) => schema.refine(
26
+ rule.test,
27
+ t(`modals.id.register.errors.${rule.key}`, { length: PASSWORD_MIN_LENGTH }),
28
+ ),
29
+ z.string().nonempty(t('error.required')) as z.ZodType<string>,
30
+ )
31
+ }
32
+
18
33
  const resetSchema = z.object({
19
- password: z
20
- .string()
21
- .nonempty(t('error.required'))
22
- .min(8, t('modals.id.register.errors.low_security', { length: 8 }))
23
- .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('modals.id.register.errors.low_security')),
24
- password_confirmation: z
25
- .string()
26
- .nonempty(t('error.required'))
27
- .min(8, t('modals.id.register.errors.low_security', { length: 8 }))
28
- .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('modals.id.register.errors.low_security')),
34
+ password: passwordField(),
35
+ password_confirmation: passwordField(),
29
36
  }).refine(
30
37
  data => data.password === data.password_confirmation,
31
38
  {
@@ -36,6 +43,10 @@ const resetSchema = z.object({
36
43
 
37
44
  const submitted = ref(false)
38
45
 
46
+ const uid = useId()
47
+ const passwordFieldId = `new-password-${uid}`
48
+ const confirmationFieldId = `new-password-confirmation-${uid}`
49
+
39
50
  const validation = computed(() => {
40
51
  if (!submitted.value) return null
41
52
  const result = resetSchema.safeParse(form)
@@ -71,7 +82,7 @@ async function onSubmit() {
71
82
  }
72
83
  }
73
84
  catch (error) {
74
- $toast.error(error?.data.message.password as string)
85
+ $toast.error(apiErrorMessage(error, t('error.validation')))
75
86
  }
76
87
  finally {
77
88
  loading.value = false
@@ -81,49 +92,79 @@ async function onSubmit() {
81
92
 
82
93
  <template>
83
94
  <form
84
- class="flex flex-col items-stretch gap-5"
95
+ class="flex flex-col gap-6 p-6"
85
96
  novalidate
86
97
  @submit.prevent="onSubmit"
87
98
  >
88
- <h2 class="text-center text-h2">
89
- {{ t('modals.id.newPassword.title') }}
90
- </h2>
91
- <p class="text-center text-body-md">
92
- {{ t('modals.id.newPassword.content') }}
93
- </p>
94
-
95
- <a-input-password
96
- v-model="form.password"
97
- :label="t('modals.id.newPassword.placeholder.1')"
98
- :error="getError('password')"
99
- />
100
-
101
- <a-alert color="blue">
102
- {{ t('modals.id.newPassword.alert') }}
103
- </a-alert>
104
-
105
- <a-input-password
106
- v-model="form.password_confirmation"
107
- :label="t('modals.id.newPassword.placeholder.2')"
108
- :error="getError('password_confirmation')"
109
- />
110
-
111
- <a-button
112
- :loading="loading"
113
- >
114
- {{ t('actions.set') }}
115
- </a-button>
116
-
117
- <p class="text-center text-body-md">
118
- {{ t('reuse.or') }}
119
- </p>
120
-
121
- <a-button
122
- type="button"
123
- view="outline"
124
- @click="newPasswordModal = false"
125
- >
126
- {{ t('actions.cancel') }}
127
- </a-button>
99
+ <div class="flex flex-col gap-1">
100
+ <h2 class="text-center text-h2 text-primary">
101
+ {{ t('modals.id.newPassword.title') }}
102
+ </h2>
103
+ <p class="text-center text-body-md text-secondary">
104
+ {{ t('modals.id.newPassword.content') }}
105
+ </p>
106
+ </div>
107
+
108
+ <div class="flex flex-col gap-4">
109
+ <div class="flex flex-col gap-1.5">
110
+ <label :for="passwordFieldId" class="text-body-md-strong text-primary">
111
+ {{ t('modals.id.newPassword.labels.password') }}
112
+ </label>
113
+ <a-input-password-v2
114
+ v-model="form.password"
115
+ :input-id="passwordFieldId"
116
+ size="lg"
117
+ autocomplete="new-password"
118
+ :start-icon="AIconLock"
119
+ :placeholder="t('modals.id.newPassword.placeholder.1')"
120
+ :error="getError('password')"
121
+ />
122
+ </div>
123
+
124
+ <a-password-requirements :password="form.password" />
125
+
126
+ <div class="flex flex-col gap-1.5">
127
+ <label :for="confirmationFieldId" class="text-body-md-strong text-primary">
128
+ {{ t('modals.id.newPassword.labels.password_confirmation') }}
129
+ </label>
130
+ <a-input-password-v2
131
+ v-model="form.password_confirmation"
132
+ :input-id="confirmationFieldId"
133
+ size="lg"
134
+ autocomplete="new-password"
135
+ :start-icon="AIconLock"
136
+ :placeholder="t('modals.id.newPassword.placeholder.2')"
137
+ :error="getError('password_confirmation')"
138
+ />
139
+ </div>
140
+
141
+ <a-button-v2
142
+ type="submit"
143
+ size="lg"
144
+ block
145
+ :loading="loading"
146
+ >
147
+ {{ t('actions.set') }}
148
+ </a-button-v2>
149
+ </div>
150
+
151
+ <div class="flex flex-col gap-4">
152
+ <div class="flex items-center gap-3">
153
+ <span class="h-px flex-1 bg-(--border-color-default)" />
154
+ <span class="shrink-0 text-body-sm text-secondary">
155
+ {{ t('reuse.or') }}
156
+ </span>
157
+ <span class="h-px flex-1 bg-(--border-color-default)" />
158
+ </div>
159
+ <a-button-v2
160
+ view="outline"
161
+ color="gray"
162
+ size="lg"
163
+ block
164
+ @click="newPasswordModal = false"
165
+ >
166
+ {{ t('actions.cancel') }}
167
+ </a-button-v2>
168
+ </div>
128
169
  </form>
129
170
  </template>
@@ -4,9 +4,9 @@ const emit = defineEmits<{
4
4
  }>()
5
5
 
6
6
  const length = 6 // Длина OTP
7
- const showError = defineModel('error')
8
- const otp = defineModel()
9
- const otpRefs = ref([])
7
+ const showError = defineModel<boolean>('error', { default: false })
8
+ const otp = defineModel<string[]>({ required: true })
9
+ const otpRefs = ref<(HTMLInputElement | null)[]>([])
10
10
  const { t } = useI18n()
11
11
 
12
12
  function checkCompletion() {
@@ -15,8 +15,8 @@ function checkCompletion() {
15
15
  }
16
16
  }
17
17
 
18
- function onInput(event, index) {
19
- const value = event.target.value
18
+ function onInput(event: Event, index: number) {
19
+ const value = (event.target as HTMLInputElement).value
20
20
  showError.value = false
21
21
 
22
22
  // Разрешить только цифры
@@ -35,7 +35,7 @@ function onInput(event, index) {
35
35
  }
36
36
  }
37
37
 
38
- function onBackspace(index, event) {
38
+ function onBackspace(index: number, event: KeyboardEvent) {
39
39
  showError.value = false
40
40
  if (otp.value[index] || index > 0) {
41
41
  if (otp.value[index] !== '') {
@@ -55,9 +55,9 @@ function onBackspace(index, event) {
55
55
  }
56
56
  }
57
57
 
58
- function onPaste(event) {
58
+ function onPaste(event: ClipboardEvent) {
59
59
  showError.value = false
60
- const pastedData = event.clipboardData.getData('text').slice(0, length)
60
+ const pastedData = (event.clipboardData?.getData('text') ?? '').slice(0, length)
61
61
 
62
62
  if (/\D/.test(pastedData)) {
63
63
  event.preventDefault()
@@ -95,9 +95,9 @@ function autoTarget() {
95
95
  }
96
96
 
97
97
  // макс сайз = 1 и если в ячейке есть цифра, она перезаписывается
98
- function inputValid(index: number, event: InputEvent) {
98
+ function inputValid(index: number, event: Event) {
99
99
  if (otp.value[index]) {
100
- event.target.value = ''
100
+ (event.target as HTMLInputElement).value = ''
101
101
  }
102
102
  }
103
103
 
@@ -121,15 +121,15 @@ function moveFocus(index: number, to: string) {
121
121
  <input
122
122
  v-for="(digit, index) in otp"
123
123
  :key="index"
124
- :ref="el => otpRefs[index] = el"
124
+ :ref="el => otpRefs[index] = el as HTMLInputElement"
125
125
  v-model="otp[index]"
126
126
  type="text"
127
127
  inputmode="numeric"
128
128
  maxlength="1"
129
- class="size-10 rounded bg-muted text-center text-lg caret-transparent focus:outline-hidden"
129
+ class="size-10 rounded-10 border border-transparent bg-muted text-center text-lg text-primary caret-transparent focus:outline-hidden"
130
130
  :class="{
131
- 'border border-danger bg-danger/10': showError,
132
- 'focus:border focus:border-focus': !showError,
131
+ 'border-danger bg-danger-subtle': showError,
132
+ 'focus:border-focus': !showError,
133
133
  }"
134
134
  @input="onInput($event, index)"
135
135
  @keydown.backspace="onBackspace(index, $event)"
@@ -4,22 +4,22 @@ const { t } = useI18n()
4
4
  </script>
5
5
 
6
6
  <template>
7
- <div class="flex flex-col gap-4 text-center">
8
- <h2 class="text-h2">
7
+ <div class="flex flex-col gap-4 p-6 text-center">
8
+ <h2 class="text-h2 text-primary">
9
9
  {{ t('modals.id.passwordSuccessful.title') }}
10
10
  </h2>
11
- <span class="mx-auto size-[100px] rounded-full bg-muted">
11
+ <div class="mx-auto flex size-24 items-center justify-center rounded-full bg-muted">
12
12
  <a-ill-ok />
13
- </span>
14
- <p class="text-body-md">
13
+ </div>
14
+ <p class="text-body-md text-secondary">
15
15
  {{ t('modals.id.passwordSuccessful.content') }}
16
16
  </p>
17
- <a-button @click="passwordSuccessfulModal = false">
17
+ <a-button-v2
18
+ size="lg"
19
+ block
20
+ @click="passwordSuccessfulModal = false"
21
+ >
18
22
  {{ t('actions.close') }}
19
- </a-button>
23
+ </a-button-v2>
20
24
  </div>
21
25
  </template>
22
-
23
- <style scoped>
24
-
25
- </style>
@@ -1,5 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { removeTrailingSlash } from '#adata-ui/components/utils/removeTrailingSlash'
3
+ import AIconMail from '#adata-ui/icons/mail.vue'
4
+ import { apiErrorMessage } from '#adata-ui/utils/apiErrorMessage'
3
5
  import * as z from 'zod'
4
6
 
5
7
  const { $toast } = useNuxtApp()
@@ -12,6 +14,8 @@ const emailField = ref('')
12
14
  const loading = ref(false)
13
15
  const submitted = ref(false)
14
16
 
17
+ const emailFieldId = `recovery-email-${useId()}`
18
+
15
19
  const emailSchema = z.object({
16
20
  emailField: z.string().nonempty(t('error.required')).email(t('error.email')),
17
21
  })
@@ -48,19 +52,13 @@ async function onSubmit() {
48
52
  resetPasswordOtpModal.value = true
49
53
  }
50
54
  catch (error) {
51
- $toast.error(error.data.message)
55
+ $toast.error(apiErrorMessage(error, t('error.validation')))
52
56
  }
53
57
  finally {
54
58
  loading.value = false
55
59
  }
56
60
  }
57
61
 
58
- function handleEnter(e: KeyboardEvent) {
59
- if (e.key === 'Enter') {
60
- onSubmit()
61
- }
62
- }
63
-
64
62
  function onCancel() {
65
63
  recoveryModal.value = false
66
64
  loginModal.value = true
@@ -71,47 +69,64 @@ watch(recoveryModal, (value) => {
71
69
  emailField.value = ''
72
70
  }
73
71
  })
74
-
75
- onMounted(() => {
76
- document.addEventListener('keyup', handleEnter)
77
- })
78
-
79
- onBeforeUnmount(() => {
80
- document.removeEventListener('keyup', handleEnter)
81
- })
82
72
  </script>
83
73
 
84
74
  <template>
85
75
  <form
86
- class="flex flex-col items-stretch gap-5 rounded-md text-center"
76
+ class="flex flex-col gap-6 p-6"
87
77
  novalidate
88
78
  @submit.prevent="onSubmit"
89
79
  >
90
- <h2 class="text-center text-h2">
91
- {{ t('modals.id.recovery.title') }}
92
- </h2>
93
- <p class="text-center text-body-md">
94
- {{ t('modals.id.recovery.content') }}
95
- </p>
96
- <a-input-standard
97
- v-model="emailField"
98
- :label="t('modals.id.recovery.placeholder')"
99
- :error="getError('emailField')"
100
- />
101
- <a-button :loading="loading">
102
- {{ t('actions.recover') }}
103
- </a-button>
104
- <p class="text-center text-body-md">
105
- {{ t('reuse.or') }}
106
- </p>
107
- <a-button
108
- type="button"
109
- view="outline"
110
- @click="onCancel"
111
- >
112
- {{ t('actions.cancel') }}
113
- </a-button>
80
+ <div class="flex flex-col gap-1">
81
+ <h2 class="text-center text-h2 text-primary">
82
+ {{ t('modals.id.recovery.title') }}
83
+ </h2>
84
+ <p class="text-center text-body-md text-secondary">
85
+ {{ t('modals.id.recovery.content') }}
86
+ </p>
87
+ </div>
88
+
89
+ <div class="flex flex-col gap-1.5">
90
+ <label :for="emailFieldId" class="text-body-md-strong text-primary">
91
+ {{ t('modals.id.recovery.label') }}
92
+ </label>
93
+ <a-input-v2
94
+ v-model="emailField"
95
+ :input-id="emailFieldId"
96
+ type="email"
97
+ size="lg"
98
+ autocomplete="email"
99
+ :start-icon="AIconMail"
100
+ :placeholder="t('modals.id.recovery.placeholder')"
101
+ :error="getError('emailField')"
102
+ />
103
+ </div>
104
+
105
+ <div class="flex flex-col gap-4">
106
+ <a-button-v2
107
+ type="submit"
108
+ size="lg"
109
+ block
110
+ :loading="loading"
111
+ >
112
+ {{ t('actions.recover') }}
113
+ </a-button-v2>
114
+ <div class="flex items-center gap-3">
115
+ <span class="h-px flex-1 bg-(--border-color-default)" />
116
+ <span class="shrink-0 text-body-sm text-secondary">
117
+ {{ t('reuse.or') }}
118
+ </span>
119
+ <span class="h-px flex-1 bg-(--border-color-default)" />
120
+ </div>
121
+ <a-button-v2
122
+ view="outline"
123
+ color="gray"
124
+ size="lg"
125
+ block
126
+ @click="onCancel"
127
+ >
128
+ {{ t('actions.cancel') }}
129
+ </a-button-v2>
130
+ </div>
114
131
  </form>
115
132
  </template>
116
-
117
- <style scoped></style>