mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #1651 from Human-Connection/1650-change_slug
Change your own slug
This commit is contained in:
commit
a55fab16d2
@ -4,7 +4,7 @@ module.exports = {
|
||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||
actorId: { type: 'string', allow: [null] },
|
||||
name: { type: 'string', disallow: [null], min: 3 },
|
||||
slug: 'string',
|
||||
slug: { type: 'string', regex: /^[a-z0-9_-]+$/, lowercase: true },
|
||||
encryptedPassword: 'string',
|
||||
avatar: { type: 'string', allow: [null] },
|
||||
coverImg: { type: 'string', allow: [null] },
|
||||
|
||||
@ -18,3 +18,67 @@ describe('role', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('slug', () => {
|
||||
it('normalizes to lowercase letters', async () => {
|
||||
const user = await instance.create('User', { slug: 'Matt' })
|
||||
await expect(user.toJson()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
slug: 'matt',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('must be unique', async done => {
|
||||
await instance.create('User', { slug: 'Matt' })
|
||||
try {
|
||||
await expect(instance.create('User', { slug: 'Matt' })).rejects.toThrow('already exists')
|
||||
done()
|
||||
} catch (error) {
|
||||
throw new Error(`
|
||||
${error}
|
||||
|
||||
Probably your database has no unique constraints!
|
||||
|
||||
To see all constraints go to http://localhost:7474/browser/ and
|
||||
paste the following:
|
||||
\`\`\`
|
||||
CALL db.constraints();
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://docs.human-connection.org/human-connection/neo4j
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
describe('characters', () => {
|
||||
const createUser = attrs => {
|
||||
return instance.create('User', attrs).then(user => user.toJson())
|
||||
}
|
||||
|
||||
it('-', async () => {
|
||||
await expect(createUser({ slug: 'matt-rider' })).resolves.toMatchObject({
|
||||
slug: 'matt-rider',
|
||||
})
|
||||
})
|
||||
|
||||
it('_', async () => {
|
||||
await expect(createUser({ slug: 'matt_rider' })).resolves.toMatchObject({
|
||||
slug: 'matt_rider',
|
||||
})
|
||||
})
|
||||
|
||||
it(' ', async () => {
|
||||
await expect(createUser({ slug: 'matt rider' })).rejects.toThrow(
|
||||
/fails to match the required pattern/,
|
||||
)
|
||||
})
|
||||
|
||||
it('ä', async () => {
|
||||
await expect(createUser({ slug: 'mätt' })).rejects.toThrow(
|
||||
/fails to match the required pattern/,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -18,6 +18,8 @@ When('I save {string} as my new name', name => {
|
||||
cy.get('[type=submit]')
|
||||
.click()
|
||||
.not('[disabled]')
|
||||
cy.get('.iziToast-message')
|
||||
.should('contain', 'Your data was successfully updated')
|
||||
})
|
||||
|
||||
When('I save {string} as my location', location => {
|
||||
@ -28,6 +30,8 @@ When('I save {string} as my location', location => {
|
||||
cy.get('[type=submit]')
|
||||
.click()
|
||||
.not('[disabled]')
|
||||
cy.get('.iziToast-message')
|
||||
.should('contain', 'Your data was successfully updated')
|
||||
myLocation = location
|
||||
})
|
||||
|
||||
@ -38,6 +42,8 @@ When('I have the following self-description:', text => {
|
||||
cy.get('[type=submit]')
|
||||
.click()
|
||||
.not('[disabled]')
|
||||
cy.get('.iziToast-message')
|
||||
.should('contain', 'Your data was successfully updated')
|
||||
aboutMeText = text
|
||||
})
|
||||
|
||||
|
||||
36
webapp/components/utils/UniqueSlugForm.js
Normal file
36
webapp/components/utils/UniqueSlugForm.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { debounce } from 'lodash'
|
||||
import { checkSlugAvailableQuery } from '~/graphql/User.js'
|
||||
|
||||
export default function UniqueSlugForm({ translate, apollo, currentUser }) {
|
||||
return {
|
||||
formSchema: {
|
||||
slug: [
|
||||
{
|
||||
type: 'string',
|
||||
required: true,
|
||||
pattern: /^[a-z0-9_-]+$/,
|
||||
message: translate('settings.validation.slug.regex'),
|
||||
},
|
||||
{
|
||||
asyncValidator(rule, value, callback) {
|
||||
debounce(() => {
|
||||
const variables = { slug: value }
|
||||
apollo.query({ query: checkSlugAvailableQuery, variables }).then(response => {
|
||||
const {
|
||||
data: { User },
|
||||
} = response
|
||||
const existingSlug = User && User[0] && User[0].slug
|
||||
const available = !existingSlug || existingSlug === currentUser.slug
|
||||
if (!available) {
|
||||
callback(new Error(translate('settings.validation.slug.alreadyTaken')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}, 500)()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
80
webapp/components/utils/UniqueSlugForm.spec.js
Normal file
80
webapp/components/utils/UniqueSlugForm.spec.js
Normal file
@ -0,0 +1,80 @@
|
||||
import UniqueSlugForm from './UniqueSlugForm'
|
||||
import Schema from 'async-validator'
|
||||
|
||||
let translate
|
||||
let apollo
|
||||
let currentUser
|
||||
|
||||
beforeEach(() => {
|
||||
translate = jest.fn(() => 'Validation error')
|
||||
apollo = {
|
||||
query: jest.fn().mockResolvedValue({ data: { User: [] } }),
|
||||
}
|
||||
currentUser = null
|
||||
})
|
||||
|
||||
describe('UniqueSlugForm', () => {
|
||||
let validate = object => {
|
||||
const { formSchema } = UniqueSlugForm({ translate, apollo, currentUser })
|
||||
const validator = new Schema(formSchema)
|
||||
return validator.validate(object, { suppressWarning: true }).catch(({ errors }) => {
|
||||
throw new Error(errors[0].message)
|
||||
})
|
||||
}
|
||||
|
||||
describe('regex', () => {
|
||||
describe('non URL-safe characters, e.g. whitespaces', () => {
|
||||
it('rejects', async () => {
|
||||
await expect(validate({ slug: 'uh oh' })).rejects.toThrow('Validation error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('alphanumeric, hyphens or underscores', () => {
|
||||
it('validates', async () => {
|
||||
await expect(validate({ slug: '_all-right_' })).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a currentUser with a slug', () => {
|
||||
beforeEach(() => {
|
||||
currentUser = { slug: 'current-user' }
|
||||
})
|
||||
|
||||
describe('backend returns no user for given slug', () => {
|
||||
beforeEach(() => {
|
||||
apollo.query.mockResolvedValue({
|
||||
data: { User: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('validates', async () => {
|
||||
await expect(validate({ slug: 'slug' })).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('backend returns user', () => {
|
||||
let slug
|
||||
beforeEach(() => {
|
||||
slug = 'already-taken'
|
||||
apollo.query.mockResolvedValue({
|
||||
data: { User: [{ slug: 'already-taken' }] },
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects', async () => {
|
||||
await expect(validate({ slug: 'uh oh' })).rejects.toThrow('Validation error')
|
||||
})
|
||||
|
||||
describe('but it is the current user', () => {
|
||||
beforeEach(() => {
|
||||
currentUser = { slug: 'already-taken' }
|
||||
})
|
||||
|
||||
it('validates', async () => {
|
||||
await expect(validate({ slug })).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -134,3 +134,11 @@ export const unfollowUserMutation = i18n => {
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const checkSlugAvailableQuery = gql`
|
||||
query($slug: String!) {
|
||||
User(slug: $slug) {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -151,11 +151,18 @@
|
||||
"data": {
|
||||
"name": "Deine Daten",
|
||||
"labelName": "Dein Name",
|
||||
"labelSlug": "Dein eindeutiger Benutzername",
|
||||
"namePlaceholder": "Petra Lustig",
|
||||
"labelCity": "Deine Stadt oder Region",
|
||||
"labelBio": "Über dich",
|
||||
"success": "Deine Daten wurden erfolgreich aktualisiert!"
|
||||
},
|
||||
"validation": {
|
||||
"slug": {
|
||||
"regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt.",
|
||||
"alreadyTaken": "Dieser Benutzername ist schon vergeben."
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"name": "Sicherheit",
|
||||
"change-password": {
|
||||
|
||||
@ -152,11 +152,18 @@
|
||||
"data": {
|
||||
"name": "Your data",
|
||||
"labelName": "Your Name",
|
||||
"labelSlug": "Your unique user name",
|
||||
"namePlaceholder": "Femanon Funny",
|
||||
"labelCity": "Your City or Region",
|
||||
"labelBio": "About You",
|
||||
"success": "Your data was successfully updated!"
|
||||
},
|
||||
"validation": {
|
||||
"slug": {
|
||||
"regex": "Allowed characters are only lowercase letters, numbers, underscores and hyphens.",
|
||||
"alreadyTaken": "This user name is already taken."
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"name": "Security",
|
||||
"change-password": {
|
||||
|
||||
@ -98,6 +98,7 @@
|
||||
"@vue/eslint-config-prettier": "~5.0.0",
|
||||
"@vue/server-test-utils": "~1.0.0-beta.29",
|
||||
"@vue/test-utils": "~1.0.0-beta.29",
|
||||
"async-validator": "^3.1.0",
|
||||
"babel-core": "~7.0.0-bridge.0",
|
||||
"babel-eslint": "~10.0.3",
|
||||
"babel-jest": "~24.9.0",
|
||||
|
||||
@ -24,6 +24,7 @@ describe('index.vue', () => {
|
||||
data: {
|
||||
UpdateUser: {
|
||||
id: 'u1',
|
||||
slug: 'peter',
|
||||
name: 'Peter',
|
||||
locationName: 'Berlin',
|
||||
about: 'Smth',
|
||||
@ -37,35 +38,68 @@ describe('index.vue', () => {
|
||||
},
|
||||
}
|
||||
getters = {
|
||||
'auth/user': () => {
|
||||
return {}
|
||||
},
|
||||
'auth/user': () => ({}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
let options
|
||||
const Wrapper = () => {
|
||||
store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return mount(index, { store, mocks, localVue })
|
||||
return mount(index, { store, mocks, localVue, ...options })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
options = {}
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(Wrapper().contains('div')).toBe(true)
|
||||
})
|
||||
|
||||
describe('given form validation errors', () => {
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
...options,
|
||||
computed: {
|
||||
formSchema: () => ({
|
||||
slug: [
|
||||
(_rule, _value, callback) => {
|
||||
callback(new Error('Ouch!'))
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('cannot call updateUser mutation', () => {
|
||||
const wrapper = Wrapper()
|
||||
|
||||
wrapper.find('#name').setValue('Peter')
|
||||
wrapper.find('.ds-form').trigger('submit')
|
||||
|
||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('no form validation errors', () => {
|
||||
beforeEach(() => {
|
||||
options = { ...options, computed: { formSchema: () => ({}) } }
|
||||
})
|
||||
|
||||
describe('given a new username and hitting submit', () => {
|
||||
it('calls updateUser mutation', () => {
|
||||
const wrapper = Wrapper()
|
||||
const input = wrapper.find('#name')
|
||||
const submitForm = wrapper.find('.ds-form')
|
||||
|
||||
input.setValue('Peter')
|
||||
submitForm.trigger('submit')
|
||||
wrapper.find('#name').setValue('Peter')
|
||||
wrapper.find('.ds-form').trigger('submit')
|
||||
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<ds-form v-model="form" @submit="submit">
|
||||
<ds-form v-model="form" :schema="formSchema" @submit="submit">
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card :header="$t('settings.data.name')">
|
||||
<ds-input
|
||||
id="name"
|
||||
@ -8,6 +9,7 @@
|
||||
:label="$t('settings.data.labelName')"
|
||||
:placeholder="$t('settings.data.namePlaceholder')"
|
||||
/>
|
||||
<ds-input id="slug" model="slug" icon="at" :label="$t('settings.data.labelSlug')" />
|
||||
<!-- eslint-disable vue/use-v-on-exact -->
|
||||
<ds-select
|
||||
id="city"
|
||||
@ -29,11 +31,19 @@
|
||||
:placeholder="$t('settings.data.labelBio')"
|
||||
/>
|
||||
<template slot="footer">
|
||||
<ds-button style="float: right;" icon="check" type="submit" :loading="loadingData" primary>
|
||||
<ds-button
|
||||
style="float: right;"
|
||||
icon="check"
|
||||
:disabled="errors"
|
||||
type="submit"
|
||||
:loading="loadingData"
|
||||
primary
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</ds-button>
|
||||
</template>
|
||||
</ds-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
@ -42,6 +52,7 @@ import gql from 'graphql-tag'
|
||||
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { CancelToken } from 'axios'
|
||||
import UniqueSlugForm from '~/components/utils/UniqueSlugForm'
|
||||
|
||||
let timeout
|
||||
const mapboxToken = process.env.MAPBOX_TOKEN
|
||||
@ -60,9 +71,10 @@ const query = gql`
|
||||
*/
|
||||
|
||||
const mutation = gql`
|
||||
mutation($id: ID!, $name: String, $locationName: String, $about: String) {
|
||||
UpdateUser(id: $id, name: $name, locationName: $locationName, about: $about) {
|
||||
mutation($id: ID!, $slug: String, $name: String, $locationName: String, $about: String) {
|
||||
UpdateUser(id: $id, slug: $slug, name: $name, locationName: $locationName, about: $about) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
locationName
|
||||
about
|
||||
@ -84,10 +96,20 @@ export default {
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
formSchema() {
|
||||
const uniqueSlugForm = UniqueSlugForm({
|
||||
apollo: this.$apollo,
|
||||
currentUser: this.currentUser,
|
||||
translate: this.$t,
|
||||
})
|
||||
return {
|
||||
...uniqueSlugForm.formSchema,
|
||||
}
|
||||
},
|
||||
form: {
|
||||
get: function() {
|
||||
const { name, locationName, about } = this.currentUser
|
||||
return { name, locationName, about }
|
||||
const { name, slug, locationName, about } = this.currentUser
|
||||
return { name, slug, locationName, about }
|
||||
},
|
||||
set: function(formData) {
|
||||
this.formData = formData
|
||||
@ -100,7 +122,7 @@ export default {
|
||||
}),
|
||||
async submit() {
|
||||
this.loadingData = true
|
||||
const { name, about } = this.formData
|
||||
const { name, slug, about } = this.formData
|
||||
let { locationName } = this.formData || this.currentUser
|
||||
locationName = locationName && (locationName['label'] || locationName)
|
||||
try {
|
||||
@ -109,14 +131,16 @@ export default {
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
name,
|
||||
slug,
|
||||
locationName,
|
||||
about,
|
||||
},
|
||||
update: (store, { data: { UpdateUser } }) => {
|
||||
const { name, locationName, about } = UpdateUser
|
||||
const { name, slug, locationName, about } = UpdateUser
|
||||
this.setCurrentUser({
|
||||
...this.currentUser,
|
||||
name,
|
||||
slug,
|
||||
locationName,
|
||||
about,
|
||||
})
|
||||
|
||||
@ -3661,6 +3661,11 @@ async-retry@^1.2.1:
|
||||
dependencies:
|
||||
retry "0.12.0"
|
||||
|
||||
async-validator@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-3.1.0.tgz#447db5eb003cbb47e650f040037a29fc3881ce92"
|
||||
integrity sha512-XyAHGwtpx3Y3aHIOaGXXFo4tiulnrh+mXBU9INxig6Q8rtmtmBxDuCxb60j7EIGbAsQg9cxfJ2jrUZ+fIqEnBQ==
|
||||
|
||||
async@^2.1.4:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user