mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
feat(webapp): location on registration (#8608)
* feat(webapp): location on registration * add location name to signup verification, allow location query * location name can be prompted in regeistration * default value null for locationName * Prevent ds-select overflow * Remove location name from label * Add margin-bottom to location-select * group location is not affected by REQUIRE_LOCATION, previous location is shown * Update webapp/components/Registration/RegistrationSlideCreate.vue Co-authored-by: Max <maxharz@gmail.com> * Replace more '16px' by '$space-small' and remove class 'password-strength' * Add class 'password-strength' again * property for previous location --------- Co-authored-by: Maximilian Harz <maxharz@gmail.com> Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
This commit is contained in:
parent
8c30098ac3
commit
df4275c5cd
@ -13,6 +13,7 @@ import existingEmailAddress from './helpers/existingEmailAddress'
|
||||
import generateNonce from './helpers/generateNonce'
|
||||
import normalizeEmail from './helpers/normalizeEmail'
|
||||
import { redeemInviteCode } from './inviteCodes'
|
||||
import { createOrUpdateLocations } from './users/location'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -43,13 +44,16 @@ export default {
|
||||
}
|
||||
args.termsAndConditionsAgreedAt = new Date().toISOString()
|
||||
|
||||
let { nonce, email, inviteCode } = args
|
||||
let { nonce, email, inviteCode, locationName } = args
|
||||
email = normalizeEmail(email)
|
||||
delete args.nonce
|
||||
delete args.email
|
||||
delete args.inviteCode
|
||||
args.encryptedPassword = await hash(args.password, 10)
|
||||
delete args.password
|
||||
delete args.locationName
|
||||
|
||||
if (locationName === '') locationName = null
|
||||
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
@ -68,6 +72,7 @@ export default {
|
||||
SET user.updatedAt = toString(datetime())
|
||||
SET user.allowEmbedIframes = false
|
||||
SET user.showShoutsPublicly = false
|
||||
SET user.locationName = $locationName
|
||||
SET email.verifiedAt = toString(datetime())
|
||||
WITH user
|
||||
OPTIONAL MATCH (post:Post)-[:IN]->(group:Group)
|
||||
@ -83,6 +88,7 @@ export default {
|
||||
nonce,
|
||||
email,
|
||||
inviteCode,
|
||||
locationName,
|
||||
},
|
||||
)
|
||||
const [user] = createUserTransactionResponse.records.map((record) => record.get('user'))
|
||||
@ -100,6 +106,7 @@ export default {
|
||||
await redeemInviteCode(context, inviteCode, true)
|
||||
}
|
||||
|
||||
await createOrUpdateLocations('User', user.id, locationName, session)
|
||||
return user
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
|
||||
@ -24,6 +24,7 @@ type Mutation {
|
||||
about: String
|
||||
termsAndConditionsAgreedVersion: String!
|
||||
locale: String
|
||||
locationName: String = null
|
||||
): User
|
||||
AddEmailAddress(email: String!): EmailAddress
|
||||
VerifyEmailAddress(
|
||||
|
||||
@ -428,7 +428,7 @@ export default shield(
|
||||
Donations: isAuthenticated,
|
||||
userData: isAuthenticated,
|
||||
VerifyNonce: allow,
|
||||
queryLocations: isAuthenticated,
|
||||
queryLocations: allow,
|
||||
availableRoles: isAdmin,
|
||||
Room: isAuthenticated,
|
||||
Message: isAuthenticated,
|
||||
|
||||
@ -9,4 +9,6 @@ BADGES_ENABLED=true
|
||||
INVITE_LINK_LIMIT=7
|
||||
NETWORK_NAME="Ocelot.social"
|
||||
|
||||
ASK_FOR_REAL_NAME=false
|
||||
ASK_FOR_REAL_NAME=false
|
||||
|
||||
REQUIRE_LOCATION=false
|
||||
@ -187,3 +187,11 @@ body.dropdown-open {
|
||||
.dropdown-arrow {
|
||||
font-size: $font-size-xx-small;
|
||||
}
|
||||
|
||||
/* Prevent ds-select overflow */
|
||||
.ds-select-value {
|
||||
max-height: 38px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@ -96,6 +96,15 @@
|
||||
</div>
|
||||
<password-strength class="password-strength" :password="formData.password" />
|
||||
|
||||
<!-- location -->
|
||||
<location-select
|
||||
v-if="locationRequired"
|
||||
class="location-select"
|
||||
v-model="locationName"
|
||||
:canBeCleared="false"
|
||||
:showPreviousLocation="false"
|
||||
/>
|
||||
|
||||
<email-display-and-verify :email="sliderData.collectedInputData.email" />
|
||||
<ds-text>
|
||||
<input
|
||||
@ -148,6 +157,7 @@ import EmailDisplayAndVerify from './EmailDisplayAndVerify'
|
||||
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink'
|
||||
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||
import ShowPassword from '../ShowPassword/ShowPassword.vue'
|
||||
import LocationSelect from '~/components/Select/LocationSelect'
|
||||
|
||||
const threePerEmSpace = ' ' // unicode u+2004;
|
||||
|
||||
@ -159,6 +169,7 @@ export default {
|
||||
PasswordStrength,
|
||||
ShowPassword,
|
||||
SweetalertIcon,
|
||||
LocationSelect,
|
||||
},
|
||||
props: {
|
||||
sliderData: { type: Object, required: true },
|
||||
@ -196,6 +207,7 @@ export default {
|
||||
// TODO: Our styleguide does not support checkmarks.
|
||||
// Integrate termsAndConditionsConfirmed into `this.formData` once we
|
||||
// have checkmarks available.
|
||||
locationName: '',
|
||||
termsAndConditionsConfirmed: false,
|
||||
receiveCommunicationAsEmailsEtcConfirmed: false,
|
||||
showPassword: false,
|
||||
@ -213,24 +225,16 @@ export default {
|
||||
this.formData.givenName = ''
|
||||
}
|
||||
} else {
|
||||
this.formData.name = this.sliderData.collectedInputData.name
|
||||
? this.sliderData.collectedInputData.name
|
||||
: ''
|
||||
this.formData.name = this.sliderData.collectedInputData.name || ''
|
||||
}
|
||||
this.formData.password = this.sliderData.collectedInputData.password
|
||||
? this.sliderData.collectedInputData.password
|
||||
: ''
|
||||
this.formData.passwordConfirmation = this.sliderData.collectedInputData.passwordConfirmation
|
||||
? this.sliderData.collectedInputData.passwordConfirmation
|
||||
: ''
|
||||
this.termsAndConditionsConfirmed = this.sliderData.collectedInputData
|
||||
.termsAndConditionsConfirmed
|
||||
? this.sliderData.collectedInputData.termsAndConditionsConfirmed
|
||||
: false
|
||||
this.receiveCommunicationAsEmailsEtcConfirmed = this.sliderData.collectedInputData
|
||||
.receiveCommunicationAsEmailsEtcConfirmed
|
||||
? this.sliderData.collectedInputData.receiveCommunicationAsEmailsEtcConfirmed
|
||||
: false
|
||||
this.formData.password = this.sliderData.collectedInputData.password || ''
|
||||
this.formData.passwordConfirmation =
|
||||
this.sliderData.collectedInputData.passwordConfirmation || ''
|
||||
this.termsAndConditionsConfirmed =
|
||||
this.sliderData.collectedInputData.termsAndConditionsConfirmed || false
|
||||
this.receiveCommunicationAsEmailsEtcConfirmed =
|
||||
this.sliderData.collectedInputData.receiveCommunicationAsEmailsEtcConfirmed || false
|
||||
this.locationName = this.sliderData.collectedInputData.locationName || ''
|
||||
this.sendValidation()
|
||||
|
||||
this.sliderData.setSliderValuesCallback(this.validInput, {
|
||||
@ -238,6 +242,16 @@ export default {
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
formLocationName() {
|
||||
// toDo: Mixin or move it to location select component
|
||||
const isNestedValue =
|
||||
typeof this.locationName === 'object' && typeof this.locationName.value === 'string'
|
||||
const isDirectString = typeof this.locationName === 'string'
|
||||
return isNestedValue ? this.locationName.value : isDirectString ? this.locationName : ''
|
||||
},
|
||||
locationRequired() {
|
||||
return this.$env.REQUIRE_LOCATION
|
||||
},
|
||||
askForRealName() {
|
||||
return this.$env.ASK_FOR_REAL_NAME
|
||||
},
|
||||
@ -249,7 +263,8 @@ export default {
|
||||
this.formData.password.length >= 1 &&
|
||||
this.formData.password === this.formData.passwordConfirmation &&
|
||||
this.termsAndConditionsConfirmed &&
|
||||
this.receiveCommunicationAsEmailsEtcConfirmed
|
||||
this.receiveCommunicationAsEmailsEtcConfirmed &&
|
||||
(this.locationRequired ? this.formLocationName : true)
|
||||
)
|
||||
},
|
||||
iconNamePassword() {
|
||||
@ -266,6 +281,9 @@ export default {
|
||||
receiveCommunicationAsEmailsEtcConfirmed() {
|
||||
this.sendValidation()
|
||||
},
|
||||
locationName() {
|
||||
this.sendValidation()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
buildName(data) {
|
||||
@ -276,6 +294,7 @@ export default {
|
||||
const { password, passwordConfirmation } = this.formData
|
||||
const name = this.buildName(this.formData)
|
||||
const { termsAndConditionsConfirmed, receiveCommunicationAsEmailsEtcConfirmed } = this
|
||||
const locationName = this.formLocationName
|
||||
|
||||
this.sliderData.setSliderValuesCallback(this.validInput, {
|
||||
collectedInputData: {
|
||||
@ -284,6 +303,7 @@ export default {
|
||||
passwordConfirmation,
|
||||
termsAndConditionsConfirmed,
|
||||
receiveCommunicationAsEmailsEtcConfirmed,
|
||||
locationName,
|
||||
},
|
||||
})
|
||||
},
|
||||
@ -299,6 +319,7 @@ export default {
|
||||
const { email, inviteCode = null, nonce } = this.sliderData.collectedInputData
|
||||
const termsAndConditionsAgreedVersion = VERSION
|
||||
const locale = this.$i18n.locale()
|
||||
|
||||
try {
|
||||
this.sliderData.setSliderValuesCallback(null, {
|
||||
sliderSettings: { buttonLoading: true },
|
||||
@ -313,6 +334,7 @@ export default {
|
||||
nonce,
|
||||
termsAndConditionsAgreedVersion,
|
||||
locale,
|
||||
locationName: this.formLocationName,
|
||||
},
|
||||
})
|
||||
this.response = 'success'
|
||||
@ -378,7 +400,7 @@ export default {
|
||||
padding-right: 0;
|
||||
height: $input-height;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: $space-small;
|
||||
|
||||
color: $text-color-base;
|
||||
background: $background-color-disabled;
|
||||
@ -400,7 +422,7 @@ export default {
|
||||
|
||||
.password-field {
|
||||
position: relative;
|
||||
padding-top: 16px;
|
||||
padding-top: $space-small;
|
||||
border: none;
|
||||
border-style: none;
|
||||
appearance: none;
|
||||
@ -410,6 +432,10 @@ export default {
|
||||
}
|
||||
|
||||
.full-name {
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: $space-small;
|
||||
}
|
||||
|
||||
.location-select {
|
||||
padding-bottom: $space-base;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -180,6 +180,7 @@ export default {
|
||||
passwordConfirmation: null,
|
||||
termsAndConditionsConfirmed: null,
|
||||
receiveCommunicationAsEmailsEtcConfirmed: null,
|
||||
locationName: null,
|
||||
},
|
||||
sliderIndex:
|
||||
this.activePage === null ? 0 : sliders.findIndex((el) => el.name === this.activePage),
|
||||
|
||||
@ -1,16 +1,31 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import LocationSelect from './LocationSelect'
|
||||
import { queryLocations } from '~/graphql/location'
|
||||
|
||||
const localVue = global.localVue
|
||||
const propsData = { value: 'nowhere' }
|
||||
|
||||
let wrapper
|
||||
|
||||
const queryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
queryLocations: [
|
||||
{
|
||||
place_name: 'Hamburg, Germany',
|
||||
place_id: 'xxx',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((string) => string),
|
||||
$i18n: {
|
||||
locale: () => 'en',
|
||||
},
|
||||
$apollo: {
|
||||
query: queryMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('LocationSelect', () => {
|
||||
@ -25,18 +40,28 @@ describe('LocationSelect', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the label', () => {
|
||||
expect(wrapper.find('label.ds-input-label').exists()).toBe(true)
|
||||
it('renders the label with previous location by default', () => {
|
||||
expect(wrapper.find('label.ds-input-label').text()).toBe('settings.data.labelCity — nowhere')
|
||||
})
|
||||
|
||||
it('renders the select', () => {
|
||||
expect(wrapper.find('.ds-select').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the clearLocationName button', () => {
|
||||
it('renders the clearLocationName button by default', () => {
|
||||
expect(wrapper.find('.base-button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('calls apollo with given value', () => {
|
||||
expect(queryMock).toBeCalledWith({
|
||||
query: queryLocations(),
|
||||
variables: {
|
||||
place: 'nowhere',
|
||||
lang: 'en',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearLocationName button click', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
@ -48,5 +73,27 @@ describe('LocationSelect', () => {
|
||||
expect(wrapper.emitted().input[0]).toEqual([''])
|
||||
})
|
||||
})
|
||||
|
||||
describe('canBeCleared is false', () => {
|
||||
beforeEach(() => {
|
||||
propsData.canBeCleared = false
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('does not show clear location name button', () => {
|
||||
expect(wrapper.find('.base-button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('showPreviousLocation is false', () => {
|
||||
beforeEach(() => {
|
||||
propsData.showPreviousLocation = false
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('does not show the previous location', () => {
|
||||
expect(wrapper.find('.ds-input-label').text()).toBe('settings.data.labelCity')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<label class="ds-input-label">
|
||||
{{ `${$t('settings.data.labelCity')}` }}
|
||||
<span v-if="locationName">{{ `— ${locationName}` }}</span>
|
||||
{{ `${$t('settings.data.labelCity')}` + locationNameLabelAddOnOldName }}
|
||||
</label>
|
||||
<ds-select
|
||||
id="city"
|
||||
@ -15,7 +14,7 @@
|
||||
@input.native="handleCityInput"
|
||||
/>
|
||||
<base-button
|
||||
v-if="locationName !== ''"
|
||||
v-if="locationName !== '' && canBeCleared"
|
||||
icon="close"
|
||||
ghost
|
||||
size="small"
|
||||
@ -36,6 +35,16 @@ export default {
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
canBeCleared: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
showPreviousLocation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
const result = await this.requestGeoData(this.locationName)
|
||||
@ -54,6 +63,9 @@ export default {
|
||||
locationName() {
|
||||
return typeof this.value === 'object' ? this.value.value : this.value
|
||||
},
|
||||
locationNameLabelAddOnOldName() {
|
||||
return this.locationName !== '' && this.showPreviousLocation ? ' — ' + this.locationName : ''
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
|
||||
@ -38,6 +38,7 @@ const options = {
|
||||
INVITE_LINK_LIMIT: process.env.INVITE_LINK_LIMIT || 7,
|
||||
NETWORK_NAME: process.env.NETWORK_NAME || 'Ocelot.social',
|
||||
ASK_FOR_REAL_NAME: process.env.ASK_FOR_REAL_NAME === 'true' || false,
|
||||
REQUIRE_LOCATION: process.env.REQUIRE_LOCATION === 'true' || false,
|
||||
}
|
||||
|
||||
const language = {
|
||||
|
||||
@ -9,6 +9,7 @@ export const SignupVerificationMutation = gql`
|
||||
$about: String
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
$locale: String
|
||||
$locationName: String
|
||||
) {
|
||||
SignupVerification(
|
||||
nonce: $nonce
|
||||
@ -19,6 +20,7 @@ export const SignupVerificationMutation = gql`
|
||||
about: $about
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locale: $locale
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
|
||||
@ -15,7 +15,11 @@
|
||||
:placeholder="$t('settings.data.namePlaceholder')"
|
||||
/>
|
||||
<ds-input id="slug" model="slug" icon="at" :label="$t('settings.data.labelSlug')" />
|
||||
<location-select v-model="formData.locationName" />
|
||||
<location-select
|
||||
class="location-selet"
|
||||
v-model="formData.locationName"
|
||||
:canBeCleared="!$env.REQUIRE_LOCATION"
|
||||
/>
|
||||
<!-- eslint-enable vue/use-v-on-exact -->
|
||||
<ds-input
|
||||
id="about"
|
||||
@ -76,6 +80,7 @@ export default {
|
||||
translate: this.$t,
|
||||
})
|
||||
return {
|
||||
locationName: { required: this.$env.REQUIRE_LOCATION },
|
||||
name: { required: true, min: 3 },
|
||||
...uniqueSlugForm.formSchema,
|
||||
}
|
||||
@ -122,4 +127,8 @@ export default {
|
||||
.location-hint {
|
||||
margin-top: -$space-x-small - $space-xxx-small - $space-xxx-small;
|
||||
}
|
||||
|
||||
.location-selet {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user