@nuxtagentic/identity 0.0.0-stage → 0.1.1
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.
- package/LICENSE.md +9 -0
- package/README.md +52 -2
- package/module.ts +61 -0
- package/package.json +47 -3
- package/runtime/components/BrandMark.vue +8 -0
- package/runtime/components/CloudMascot.vue +34 -0
- package/runtime/components/CloudconfigLogo.vue +34 -0
- package/runtime/components/GithubAuthPanel.vue +158 -0
- package/runtime/components/ProfileSettings.vue +130 -0
- package/runtime/components/SettingsSection.vue +17 -0
- package/runtime/components/SocialAuthButton.vue +48 -0
- package/runtime/components/UserAvatar.vue +37 -0
- package/runtime/components/WorkspaceSwitcher.vue +118 -0
- package/runtime/components/settings/ImageField.vue +106 -0
- package/runtime/components/settings/OrganizationLogoEditor.vue +149 -0
- package/runtime/components/settings/Provisioning.vue +170 -0
- package/runtime/components/settings/Security.vue +211 -0
- package/runtime/components/settings/SettingsOrganizations.vue +305 -0
- package/runtime/components/settings/SingleSignOn.vue +396 -0
- package/runtime/components/settings/TwoFactor.vue +256 -0
- package/runtime/composables/useCurrentUser.ts +69 -0
- package/runtime/composables/useIdentityImageUpload.ts +31 -0
- package/runtime/composables/useOrganizationAccess.ts +46 -0
- package/runtime/composables/useProfileNamePreview.ts +4 -0
- package/runtime/composables/useWorkspaceScope.ts +12 -0
- package/runtime/composables/useWorkspaces.ts +108 -0
- package/runtime/layouts/default.vue +20 -0
- package/runtime/middleware/auth.ts +22 -0
- package/runtime/pages/organizations.vue +3 -0
- package/runtime/pages/sign-in.vue +7 -0
- package/runtime/pages/sign-up.vue +18 -0
- package/runtime/pages/two-factor.vue +148 -0
- package/runtime/settings-ui.ts +22 -0
- package/runtime/shared/agent.ts +1 -0
- package/runtime/utils/format.ts +18 -0
- package/runtime/workspace.ts +8 -0
- package/schema.ts +29 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCloudConfigAccount, navigateTo, useRuntimeConfig } from '#imports'
|
|
3
|
+
import { useCurrentUser } from '../../composables/useCurrentUser'
|
|
4
|
+
import { ref, computed, onMounted } from 'vue'
|
|
5
|
+
|
|
6
|
+
import type { AuthAccount } from '@cloudconfig/core/client'
|
|
7
|
+
|
|
8
|
+
defineOptions({ name: 'SettingsSecurity' })
|
|
9
|
+
|
|
10
|
+
const account = useCloudConfigAccount()
|
|
11
|
+
const authConfig = useRuntimeConfig().public.cloudconfig as { twoFactor?: unknown }
|
|
12
|
+
const isTwoFactorEnabled = Boolean(authConfig.twoFactor)
|
|
13
|
+
const user = useCurrentUser()
|
|
14
|
+
const currentPassword = ref('')
|
|
15
|
+
const newPassword = ref('')
|
|
16
|
+
const deletePassword = ref('')
|
|
17
|
+
const isBusy = ref(false)
|
|
18
|
+
const isLoaded = ref(false)
|
|
19
|
+
const message = ref('')
|
|
20
|
+
const error = ref('')
|
|
21
|
+
const accounts = ref<AuthAccount[]>([])
|
|
22
|
+
const hasPassword = computed(() => accounts.value.some((item) => item.providerId === 'credential'))
|
|
23
|
+
const fieldUi = {
|
|
24
|
+
root: 'w-full',
|
|
25
|
+
base: 'h-10 rounded-md bg-(--gray-100) px-3 py-0 text-sm font-normal text-highlighted ring-(--gray-500) focus-visible:ring-highlighted',
|
|
26
|
+
}
|
|
27
|
+
const formFieldUi = {
|
|
28
|
+
label: 'text-[13px] font-normal text-muted',
|
|
29
|
+
container: 'mt-1.5',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onMounted(async () => {
|
|
33
|
+
try {
|
|
34
|
+
accounts.value = await account.listAccounts()
|
|
35
|
+
isLoaded.value = true
|
|
36
|
+
} catch (cause) {
|
|
37
|
+
error.value =
|
|
38
|
+
cause instanceof Error
|
|
39
|
+
? cause.message
|
|
40
|
+
: 'Could not load security settings. Refresh to try again.'
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
async function changePassword() {
|
|
45
|
+
if (isBusy.value || !currentPassword.value || newPassword.value.length < 8) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
isBusy.value = true
|
|
49
|
+
error.value = ''
|
|
50
|
+
message.value = ''
|
|
51
|
+
try {
|
|
52
|
+
await account.changePassword({
|
|
53
|
+
currentPassword: currentPassword.value,
|
|
54
|
+
newPassword: newPassword.value,
|
|
55
|
+
revokeOtherSessions: true,
|
|
56
|
+
})
|
|
57
|
+
currentPassword.value = ''
|
|
58
|
+
newPassword.value = ''
|
|
59
|
+
message.value = 'Password changed and other sessions revoked.'
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
error.value = cause instanceof Error ? cause.message : 'Could not change your password.'
|
|
62
|
+
} finally {
|
|
63
|
+
isBusy.value = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function removeAccount() {
|
|
68
|
+
if (
|
|
69
|
+
isBusy.value ||
|
|
70
|
+
!isLoaded.value ||
|
|
71
|
+
!window.confirm(
|
|
72
|
+
'Permanently delete your account, sessions, and linked sign-in methods? This cannot be undone.',
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
isBusy.value = true
|
|
78
|
+
error.value = ''
|
|
79
|
+
message.value = ''
|
|
80
|
+
try {
|
|
81
|
+
await account.deleteUser(deletePassword.value ? { password: deletePassword.value } : {})
|
|
82
|
+
user.value = null
|
|
83
|
+
await navigateTo('/sign-in')
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
error.value =
|
|
86
|
+
cause instanceof Error
|
|
87
|
+
? cause.message
|
|
88
|
+
: 'Could not delete your account. Sign in again and retry.'
|
|
89
|
+
isBusy.value = false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<div
|
|
96
|
+
id="security"
|
|
97
|
+
class="scroll-mt-6"
|
|
98
|
+
>
|
|
99
|
+
<SettingsTwoFactor
|
|
100
|
+
v-if="isTwoFactorEnabled"
|
|
101
|
+
:has-password="hasPassword"
|
|
102
|
+
/>
|
|
103
|
+
<SettingsSection
|
|
104
|
+
title="Security"
|
|
105
|
+
description="Manage how you sign in."
|
|
106
|
+
>
|
|
107
|
+
<p
|
|
108
|
+
v-if="error"
|
|
109
|
+
role="alert"
|
|
110
|
+
class="text-[13px] text-error"
|
|
111
|
+
>
|
|
112
|
+
{{ error }}
|
|
113
|
+
</p>
|
|
114
|
+
<p
|
|
115
|
+
v-if="message"
|
|
116
|
+
role="status"
|
|
117
|
+
class="text-[13px] text-muted"
|
|
118
|
+
>
|
|
119
|
+
{{ message }}
|
|
120
|
+
</p>
|
|
121
|
+
<form
|
|
122
|
+
v-if="hasPassword"
|
|
123
|
+
class="grid gap-4"
|
|
124
|
+
@submit.prevent="changePassword"
|
|
125
|
+
>
|
|
126
|
+
<UFormField
|
|
127
|
+
label="Current password"
|
|
128
|
+
:ui="formFieldUi"
|
|
129
|
+
>
|
|
130
|
+
<UInput
|
|
131
|
+
v-model="currentPassword"
|
|
132
|
+
type="password"
|
|
133
|
+
autocomplete="current-password"
|
|
134
|
+
required
|
|
135
|
+
:disabled="isBusy"
|
|
136
|
+
:ui="fieldUi"
|
|
137
|
+
/>
|
|
138
|
+
</UFormField>
|
|
139
|
+
<UFormField
|
|
140
|
+
label="New password"
|
|
141
|
+
:ui="formFieldUi"
|
|
142
|
+
>
|
|
143
|
+
<UInput
|
|
144
|
+
v-model="newPassword"
|
|
145
|
+
type="password"
|
|
146
|
+
autocomplete="new-password"
|
|
147
|
+
required
|
|
148
|
+
minlength="8"
|
|
149
|
+
:disabled="isBusy"
|
|
150
|
+
:ui="fieldUi"
|
|
151
|
+
/>
|
|
152
|
+
</UFormField>
|
|
153
|
+
<p class="text-[13px] text-muted">Changing your password signs out your other sessions.</p>
|
|
154
|
+
<UButton
|
|
155
|
+
type="submit"
|
|
156
|
+
label="Change password"
|
|
157
|
+
:loading="isBusy"
|
|
158
|
+
:disabled="!currentPassword || newPassword.length < 8"
|
|
159
|
+
class="h-10 w-fit rounded-md px-4 text-sm font-medium"
|
|
160
|
+
/>
|
|
161
|
+
</form>
|
|
162
|
+
<p
|
|
163
|
+
v-else-if="isLoaded"
|
|
164
|
+
class="text-sm text-muted"
|
|
165
|
+
>
|
|
166
|
+
Your sign-in provider manages your password.
|
|
167
|
+
</p>
|
|
168
|
+
<p
|
|
169
|
+
v-else-if="!error"
|
|
170
|
+
role="status"
|
|
171
|
+
class="text-[13px] text-muted"
|
|
172
|
+
>
|
|
173
|
+
Loading security settings…
|
|
174
|
+
</p>
|
|
175
|
+
</SettingsSection>
|
|
176
|
+
<SettingsSection
|
|
177
|
+
title="Delete account"
|
|
178
|
+
description="Permanently remove your profile, sessions, and linked sign-in accounts."
|
|
179
|
+
>
|
|
180
|
+
<UFormField
|
|
181
|
+
v-if="hasPassword"
|
|
182
|
+
label="Password"
|
|
183
|
+
:ui="formFieldUi"
|
|
184
|
+
>
|
|
185
|
+
<UInput
|
|
186
|
+
v-model="deletePassword"
|
|
187
|
+
type="password"
|
|
188
|
+
autocomplete="current-password"
|
|
189
|
+
:disabled="isBusy"
|
|
190
|
+
:ui="fieldUi"
|
|
191
|
+
/>
|
|
192
|
+
</UFormField>
|
|
193
|
+
<p
|
|
194
|
+
v-else-if="isLoaded"
|
|
195
|
+
class="text-[13px] text-muted"
|
|
196
|
+
>
|
|
197
|
+
Sign in again before deleting your account.
|
|
198
|
+
</p>
|
|
199
|
+
<UButton
|
|
200
|
+
type="button"
|
|
201
|
+
color="error"
|
|
202
|
+
variant="outline"
|
|
203
|
+
label="Delete my account"
|
|
204
|
+
:loading="isBusy"
|
|
205
|
+
:disabled="!isLoaded"
|
|
206
|
+
class="h-10 w-fit rounded-md px-4 text-sm font-normal"
|
|
207
|
+
@click="removeAccount"
|
|
208
|
+
/>
|
|
209
|
+
</SettingsSection>
|
|
210
|
+
</div>
|
|
211
|
+
</template>
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useCloudConfigOrganizations, useState, useRoute, navigateTo } from '#imports'
|
|
3
|
+
import { ref, shallowRef, computed, useTemplateRef, watch, onMounted, nextTick } from 'vue'
|
|
4
|
+
|
|
5
|
+
import type { Organization } from '@cloudconfig/core/client'
|
|
6
|
+
import { useWorkspaceOrganizations } from '../../composables/useWorkspaces'
|
|
7
|
+
import { useIdentityImageUpload } from '../../composables/useIdentityImageUpload'
|
|
8
|
+
import OrganizationLogoEditor from './OrganizationLogoEditor.vue'
|
|
9
|
+
|
|
10
|
+
const { active = true } = defineProps<{ active?: boolean }>()
|
|
11
|
+
const organizations = useCloudConfigOrganizations()
|
|
12
|
+
const activeOrganization = useState<Organization | null>('active-organization', () => null)
|
|
13
|
+
const route = useRoute()
|
|
14
|
+
const { data: items, pending, error: loadError, refresh } = useWorkspaceOrganizations()
|
|
15
|
+
const isSaving = ref(false)
|
|
16
|
+
const saveError = ref('')
|
|
17
|
+
const error = computed(
|
|
18
|
+
() => saveError.value || (loadError.value ? 'Could not load organizations.' : ''),
|
|
19
|
+
)
|
|
20
|
+
const name = ref('')
|
|
21
|
+
const slug = ref('')
|
|
22
|
+
const logo = ref<string | null>(null)
|
|
23
|
+
const logoFile = shallowRef<File | null>(null)
|
|
24
|
+
const imageUpload = useIdentityImageUpload()
|
|
25
|
+
const creating = computed(() => route.query.create === 'true')
|
|
26
|
+
const editingOrganization = computed(() =>
|
|
27
|
+
creating.value
|
|
28
|
+
? null
|
|
29
|
+
: items.value.find((item) => String(item.id) === route.query.organizationLogo),
|
|
30
|
+
)
|
|
31
|
+
const nameInput = useTemplateRef('nameInput')
|
|
32
|
+
const fieldUi = {
|
|
33
|
+
root: 'w-full',
|
|
34
|
+
base: 'h-10 rounded-md bg-(--gray-100) px-3 py-0 text-sm font-normal text-highlighted ring-(--gray-500) focus-visible:ring-highlighted',
|
|
35
|
+
}
|
|
36
|
+
const formFieldUi = {
|
|
37
|
+
label: 'text-[13px] font-normal text-muted',
|
|
38
|
+
container: 'mt-1.5',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function updateName(value: string) {
|
|
42
|
+
name.value = value
|
|
43
|
+
slug.value = value
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.trim()
|
|
46
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
47
|
+
.replace(/(^-|-$)/g, '')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function showEditor() {
|
|
51
|
+
const query = { ...route.query }
|
|
52
|
+
delete query.organizationLogo
|
|
53
|
+
query.create = 'true'
|
|
54
|
+
return navigateTo({
|
|
55
|
+
path: '/dashboard/settings',
|
|
56
|
+
query,
|
|
57
|
+
hash: '#organizations',
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function editLogo(item: Organization) {
|
|
62
|
+
const query = { ...route.query }
|
|
63
|
+
delete query.create
|
|
64
|
+
query.organizationLogo = String(item.id)
|
|
65
|
+
return navigateTo({ path: '/dashboard/settings', query, hash: '#organizations' })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function closeEditor() {
|
|
69
|
+
const query = { ...route.query }
|
|
70
|
+
delete query.create
|
|
71
|
+
delete query.organizationLogo
|
|
72
|
+
name.value = ''
|
|
73
|
+
slug.value = ''
|
|
74
|
+
logo.value = null
|
|
75
|
+
logoFile.value = null
|
|
76
|
+
imageUpload.clear()
|
|
77
|
+
saveError.value = ''
|
|
78
|
+
return navigateTo({ path: '/dashboard/settings', query, hash: '#organizations' })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function finishLogoEdit(updatedOrganization: Organization) {
|
|
82
|
+
items.value = items.value.map((item) =>
|
|
83
|
+
item.id === updatedOrganization.id ? updatedOrganization : item,
|
|
84
|
+
)
|
|
85
|
+
if (activeOrganization.value?.id === updatedOrganization.id) {
|
|
86
|
+
activeOrganization.value = updatedOrganization
|
|
87
|
+
}
|
|
88
|
+
return closeEditor()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function createOrganization() {
|
|
92
|
+
if (isSaving.value || !name.value.trim() || !slug.value.trim()) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
isSaving.value = true
|
|
96
|
+
saveError.value = ''
|
|
97
|
+
try {
|
|
98
|
+
const image = logoFile.value ? await imageUpload.upload(logoFile.value) : logo.value
|
|
99
|
+
activeOrganization.value = await organizations.create({
|
|
100
|
+
name: name.value.trim(),
|
|
101
|
+
slug: slug.value.trim(),
|
|
102
|
+
logo: image,
|
|
103
|
+
})
|
|
104
|
+
await closeEditor()
|
|
105
|
+
await refresh()
|
|
106
|
+
} catch (cause) {
|
|
107
|
+
saveError.value = cause instanceof Error ? cause.message : 'Could not create the organization.'
|
|
108
|
+
} finally {
|
|
109
|
+
isSaving.value = false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function activate(item: Organization) {
|
|
114
|
+
if (isSaving.value) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
isSaving.value = true
|
|
118
|
+
saveError.value = ''
|
|
119
|
+
try {
|
|
120
|
+
activeOrganization.value = await organizations.setActive(item.id)
|
|
121
|
+
} catch (cause) {
|
|
122
|
+
saveError.value = cause instanceof Error ? cause.message : 'Could not select the organization.'
|
|
123
|
+
} finally {
|
|
124
|
+
isSaving.value = false
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
watch(
|
|
129
|
+
() => active && creating.value,
|
|
130
|
+
async (value) => {
|
|
131
|
+
if (!value) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
await nextTick()
|
|
135
|
+
nameInput.value?.inputRef?.focus()
|
|
136
|
+
},
|
|
137
|
+
{ flush: 'post' },
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
if (active && creating.value) {
|
|
142
|
+
nameInput.value?.inputRef?.focus()
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<template>
|
|
148
|
+
<SettingsSection
|
|
149
|
+
id="organizations"
|
|
150
|
+
title="Organization"
|
|
151
|
+
description="Manage your shared workspaces."
|
|
152
|
+
class="scroll-mt-6"
|
|
153
|
+
>
|
|
154
|
+
<p
|
|
155
|
+
v-if="error"
|
|
156
|
+
role="alert"
|
|
157
|
+
class="text-[13px] text-error"
|
|
158
|
+
>
|
|
159
|
+
{{ error }}
|
|
160
|
+
</p>
|
|
161
|
+
<div
|
|
162
|
+
:aria-busy="pending"
|
|
163
|
+
class="grid gap-4"
|
|
164
|
+
>
|
|
165
|
+
<div
|
|
166
|
+
v-for="item in items"
|
|
167
|
+
:key="item.id"
|
|
168
|
+
class="flex flex-wrap items-center justify-between gap-3"
|
|
169
|
+
>
|
|
170
|
+
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
171
|
+
<UAvatar
|
|
172
|
+
:src="item.logo ?? undefined"
|
|
173
|
+
:alt="item.name"
|
|
174
|
+
:text="item.name.charAt(0).toUpperCase()"
|
|
175
|
+
size="md"
|
|
176
|
+
:ui="{ root: 'rounded-md bg-(--gray-500)', image: 'object-contain' }"
|
|
177
|
+
/>
|
|
178
|
+
<div class="min-w-0">
|
|
179
|
+
<p class="truncate text-sm text-highlighted">{{ item.name }}</p>
|
|
180
|
+
<p class="mt-0.5 truncate text-[13px] text-muted">{{ item.slug }}</p>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
184
|
+
<UButton
|
|
185
|
+
type="button"
|
|
186
|
+
color="neutral"
|
|
187
|
+
variant="outline"
|
|
188
|
+
label="Edit logo"
|
|
189
|
+
:aria-label="`Edit logo for ${item.name}`"
|
|
190
|
+
:disabled="isSaving"
|
|
191
|
+
class="h-9 rounded-md px-3 text-sm font-normal"
|
|
192
|
+
@click="editLogo(item)"
|
|
193
|
+
/>
|
|
194
|
+
<UButton
|
|
195
|
+
type="button"
|
|
196
|
+
color="neutral"
|
|
197
|
+
variant="outline"
|
|
198
|
+
:label="activeOrganization?.id === item.id ? 'Active' : 'Use organization'"
|
|
199
|
+
:disabled="isSaving || activeOrganization?.id === item.id"
|
|
200
|
+
class="h-9 rounded-md px-3 text-sm font-normal"
|
|
201
|
+
@click="activate(item)"
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<p
|
|
206
|
+
v-if="pending"
|
|
207
|
+
role="status"
|
|
208
|
+
class="text-[13px] text-muted"
|
|
209
|
+
>
|
|
210
|
+
Loading organizations…
|
|
211
|
+
</p>
|
|
212
|
+
<p
|
|
213
|
+
v-else-if="!items.length && !error"
|
|
214
|
+
class="text-sm text-muted"
|
|
215
|
+
>
|
|
216
|
+
No organizations yet. Create one to start a shared workspace.
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
<OrganizationLogoEditor
|
|
220
|
+
v-if="editingOrganization"
|
|
221
|
+
:key="editingOrganization.id"
|
|
222
|
+
:organization="editingOrganization"
|
|
223
|
+
@saved="finishLogoEdit"
|
|
224
|
+
@cancel="closeEditor"
|
|
225
|
+
@update:saving="isSaving = $event"
|
|
226
|
+
/>
|
|
227
|
+
<form
|
|
228
|
+
v-if="creating"
|
|
229
|
+
class="grid gap-4"
|
|
230
|
+
@submit.prevent="createOrganization"
|
|
231
|
+
@keydown.esc.stop.prevent="!isSaving && closeEditor()"
|
|
232
|
+
>
|
|
233
|
+
<UFormField
|
|
234
|
+
label="Logo"
|
|
235
|
+
:ui="formFieldUi"
|
|
236
|
+
>
|
|
237
|
+
<SettingsImageField
|
|
238
|
+
v-model:file="logoFile"
|
|
239
|
+
v-model:image="logo"
|
|
240
|
+
kind="logo"
|
|
241
|
+
:name="name"
|
|
242
|
+
:disabled="isSaving"
|
|
243
|
+
/>
|
|
244
|
+
</UFormField>
|
|
245
|
+
<UFormField
|
|
246
|
+
label="Name"
|
|
247
|
+
:ui="formFieldUi"
|
|
248
|
+
>
|
|
249
|
+
<UInput
|
|
250
|
+
ref="nameInput"
|
|
251
|
+
:model-value="name"
|
|
252
|
+
required
|
|
253
|
+
:disabled="isSaving"
|
|
254
|
+
:ui="fieldUi"
|
|
255
|
+
@update:model-value="updateName"
|
|
256
|
+
/>
|
|
257
|
+
</UFormField>
|
|
258
|
+
<UFormField
|
|
259
|
+
label="Slug"
|
|
260
|
+
:ui="formFieldUi"
|
|
261
|
+
>
|
|
262
|
+
<UInput
|
|
263
|
+
v-model="slug"
|
|
264
|
+
required
|
|
265
|
+
pattern="[a-z0-9-]+"
|
|
266
|
+
:disabled="isSaving"
|
|
267
|
+
:ui="fieldUi"
|
|
268
|
+
/>
|
|
269
|
+
</UFormField>
|
|
270
|
+
<div class="flex flex-wrap gap-2">
|
|
271
|
+
<UButton
|
|
272
|
+
type="submit"
|
|
273
|
+
label="Create organization"
|
|
274
|
+
:loading="isSaving"
|
|
275
|
+
:disabled="!name.trim() || !slug.trim()"
|
|
276
|
+
class="h-10 rounded-md px-4 text-sm font-medium"
|
|
277
|
+
/>
|
|
278
|
+
<UButton
|
|
279
|
+
type="button"
|
|
280
|
+
color="neutral"
|
|
281
|
+
variant="outline"
|
|
282
|
+
label="Cancel"
|
|
283
|
+
:disabled="isSaving"
|
|
284
|
+
class="h-10 rounded-md px-4 text-sm font-normal"
|
|
285
|
+
@click="closeEditor"
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
</form>
|
|
289
|
+
<UButton
|
|
290
|
+
v-else
|
|
291
|
+
type="button"
|
|
292
|
+
color="neutral"
|
|
293
|
+
variant="outline"
|
|
294
|
+
icon="i-lucide-plus"
|
|
295
|
+
label="New organization"
|
|
296
|
+
:disabled="isSaving"
|
|
297
|
+
class="h-10 w-fit rounded-md px-4 text-sm font-normal"
|
|
298
|
+
@click="showEditor"
|
|
299
|
+
/>
|
|
300
|
+
</SettingsSection>
|
|
301
|
+
<template v-if="active">
|
|
302
|
+
<SettingsSingleSignOn />
|
|
303
|
+
<SettingsProvisioning />
|
|
304
|
+
</template>
|
|
305
|
+
</template>
|