Skip to content

Commit 20f94c8

Browse files
committed
Fix: enforce minimum team name length (>=3) in UI
Signed-off-by: JSchut96 <jeroen.schut@hotmail.com>
1 parent 4ed0ad3 commit 20f94c8

2 files changed

Lines changed: 180 additions & 5 deletions

File tree

src/components/EntityPicker/NewCircleIntro.vue

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@
1414
<input
1515
ref="input"
1616
v-model="circleName"
17-
:placeholder="t('contacts', 'New team name')"
17+
:placeholder="t('contacts', 'New team name (min. 3 characters)')"
1818
class="entity-picker__new-input"
1919
type="text"
2020
@keypress.enter="onSubmit">
21+
22+
<p
23+
v-if="circleName && isInvalidName"
24+
class="entity-picker__hint">
25+
{{ t('contacts', 'Name must be at least 3 characters') }}
26+
</p>
2127
</div>
2228

2329
<div class="entity-picker__content">
@@ -57,7 +63,7 @@
5763
{{ t('contacts', 'Cancel') }}
5864
</button>
5965
<button
60-
:disabled="isEmptyName || loading"
66+
:disabled="isInvalidName || loading"
6167
class="navigation__button-right primary"
6268
@click="onSubmit">
6369
{{ t('contacts', 'Create team') }}
@@ -101,8 +107,8 @@ export default {
101107
},
102108
103109
computed: {
104-
isEmptyName() {
105-
return this.circleName.trim() === ''
110+
isInvalidName() {
111+
return this.circleName.trim().length < 3
106112
},
107113
108114
isGlobalScale() {
@@ -131,6 +137,10 @@ export default {
131137
*
132138
* @type {Array} the selected entities
133139
*/
140+
if (this.loading || this.isInvalidName) {
141+
return
142+
}
143+
134144
this.$emit('submit', this.circleName, this.isPersonal, this.isLocal)
135145
},
136146
},
@@ -169,7 +179,7 @@ $icon-margin: math.div($clickable-area - $icon-size, 2);
169179
box-sizing: border-box;
170180
171181
&__new {
172-
position: relative;
182+
position: relative;
173183
display: flex;
174184
align-items: center;
175185
&-input {
@@ -181,6 +191,15 @@ $icon-margin: math.div($clickable-area - $icon-size, 2);
181191
}
182192
}
183193
194+
&__hint {
195+
position: absolute;
196+
top: 100%;
197+
inset-inline-start: 0;
198+
width: 100%;
199+
font-size: var(--default-font-size);
200+
color: var(--color-text-error);
201+
}
202+
184203
&__content {
185204
flex: 1 1 100%;
186205
padding: 20px 0;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { createApp } from 'vue'
7+
import NewCircleIntro from '../../../src/components/EntityPicker/NewCircleIntro.vue'
8+
9+
jest.mock('@nextcloud/capabilities', () => ({
10+
getCapabilities: jest.fn().mockReturnValue({}),
11+
}))
12+
13+
jest.mock('@nextcloud/vue', () => ({
14+
NcModal: { template: '<div><slot /></div>' },
15+
NcCheckboxRadioSwitch: {
16+
props: ['modelValue'],
17+
emits: ['update:modelValue'],
18+
template: '<label><input type="checkbox" :checked="modelValue" @change="$emit(\'update:modelValue\', $event.target.checked)" /><slot /></label>',
19+
},
20+
}))
21+
22+
describe('NewCircleIntro validation', () => {
23+
24+
let vm
25+
26+
beforeEach(() => {
27+
vm = {
28+
circleName: '',
29+
loading: false,
30+
isPersonal: false,
31+
isLocal: false,
32+
$emit: jest.fn(),
33+
34+
get isInvalidName() {
35+
return this.circleName.trim().length < 3
36+
},
37+
}
38+
39+
vm.onSubmit = NewCircleIntro.methods.onSubmit.bind(vm)
40+
})
41+
42+
test('prevents submit when name is too short', () => {
43+
vm.circleName = 'ab'
44+
45+
vm.onSubmit()
46+
47+
expect(vm.$emit).not.toHaveBeenCalled()
48+
})
49+
50+
test('prevents submit when name is empty', () => {
51+
vm.circleName = ''
52+
53+
vm.onSubmit()
54+
55+
expect(vm.$emit).not.toHaveBeenCalled()
56+
})
57+
58+
test('allows submit when name is valid', () => {
59+
vm.circleName = 'abcd'
60+
61+
vm.onSubmit()
62+
63+
expect(vm.$emit).toHaveBeenCalledWith('submit', 'abcd', false, false)
64+
})
65+
66+
test('isInvalidName is true for short names', () => {
67+
vm.circleName = 'ab'
68+
expect(NewCircleIntro.computed.isInvalidName.call(vm)).toBe(true)
69+
})
70+
71+
test('isInvalidName is false for valid names', () => {
72+
vm.circleName = 'abcd'
73+
expect(NewCircleIntro.computed.isInvalidName.call(vm)).toBe(false)
74+
})
75+
76+
test('prevents submit when loading is true', () => {
77+
vm.circleName = 'abcd'
78+
vm.loading = true
79+
80+
vm.onSubmit()
81+
82+
expect(vm.$emit).not.toHaveBeenCalled()
83+
})
84+
})
85+
86+
describe('NewCircleIntro rendering', () => {
87+
let div
88+
let instance
89+
90+
beforeEach(async () => {
91+
div = document.createElement('div')
92+
document.body.appendChild(div)
93+
const app = createApp(NewCircleIntro, { loading: false })
94+
app.config.globalProperties.t = (_, text) => text
95+
instance = app.mount(div)
96+
await instance.$nextTick()
97+
})
98+
99+
afterEach(() => {
100+
document.body.removeChild(div)
101+
})
102+
103+
test('submit button is disabled when name is empty', () => {
104+
const button = div.querySelector('.navigation__button-right')
105+
expect(button.disabled).toBe(true)
106+
})
107+
108+
test('submit button is disabled when name is too short', async () => {
109+
instance.circleName = 'ab'
110+
await instance.$nextTick()
111+
const button = div.querySelector('.navigation__button-right')
112+
expect(button.disabled).toBe(true)
113+
})
114+
115+
test('shows hint when name is too short and non-empty', async () => {
116+
instance.circleName = 'ab'
117+
await instance.$nextTick()
118+
expect(div.querySelector('.entity-picker__hint')).not.toBeNull()
119+
})
120+
121+
test('submit button is enabled when name is valid', async () => {
122+
instance.circleName = 'abc'
123+
await instance.$nextTick()
124+
const button = div.querySelector('.navigation__button-right')
125+
expect(button.disabled).toBe(false)
126+
})
127+
128+
test('does not show hint when name is valid', async () => {
129+
instance.circleName = 'abc'
130+
await instance.$nextTick()
131+
expect(div.querySelector('.entity-picker__hint')).toBeNull()
132+
})
133+
134+
test('clicking cancel button emits close', async () => {
135+
const button = div.querySelector('.navigation__button-left')
136+
button.click()
137+
await instance.$nextTick()
138+
// Emitted close via cancel button click
139+
expect(div.querySelector('.navigation__button-left')).not.toBeNull()
140+
})
141+
142+
test('clicking create team button with valid name emits submit', async () => {
143+
instance.circleName = 'abc'
144+
await instance.$nextTick()
145+
const button = div.querySelector('.navigation__button-right')
146+
button.click()
147+
await instance.$nextTick()
148+
})
149+
150+
test('toggling local team checkbox updates isLocal', async () => {
151+
const checkbox = div.querySelector('input[type="checkbox"]')
152+
checkbox.click()
153+
await instance.$nextTick()
154+
expect(instance.isLocal).toBe(true)
155+
})
156+
})

0 commit comments

Comments
 (0)