diff --git a/backend/src/graphql/resolvers/registration.ts b/backend/src/graphql/resolvers/registration.ts index fb8e83ec2..db24ed7d0 100644 --- a/backend/src/graphql/resolvers/registration.ts +++ b/backend/src/graphql/resolvers/registration.ts @@ -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') diff --git a/backend/src/graphql/types/type/EmailAddress.gql b/backend/src/graphql/types/type/EmailAddress.gql index 261b97207..3251ff9dd 100644 --- a/backend/src/graphql/types/type/EmailAddress.gql +++ b/backend/src/graphql/types/type/EmailAddress.gql @@ -24,6 +24,7 @@ type Mutation { about: String termsAndConditionsAgreedVersion: String! locale: String + locationName: String = null ): User AddEmailAddress(email: String!): EmailAddress VerifyEmailAddress( diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 135c7553c..cc70fc00b 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -428,7 +428,7 @@ export default shield( Donations: isAuthenticated, userData: isAuthenticated, VerifyNonce: allow, - queryLocations: isAuthenticated, + queryLocations: allow, availableRoles: isAdmin, Room: isAuthenticated, Message: isAuthenticated, diff --git a/webapp/.env.template b/webapp/.env.template index 87ba3189c..344bf8b43 100644 --- a/webapp/.env.template +++ b/webapp/.env.template @@ -9,4 +9,6 @@ BADGES_ENABLED=true INVITE_LINK_LIMIT=7 NETWORK_NAME="Ocelot.social" -ASK_FOR_REAL_NAME=false \ No newline at end of file +ASK_FOR_REAL_NAME=false + +REQUIRE_LOCATION=false \ No newline at end of file diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index f912a0dc3..fdf5c4240 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -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; +} diff --git a/webapp/components/Registration/RegistrationSlideCreate.vue b/webapp/components/Registration/RegistrationSlideCreate.vue index 1777e617b..10eb2cfaa 100644 --- a/webapp/components/Registration/RegistrationSlideCreate.vue +++ b/webapp/components/Registration/RegistrationSlideCreate.vue @@ -96,6 +96,15 @@ + + + = 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; } diff --git a/webapp/components/Registration/RegistrationSlider.vue b/webapp/components/Registration/RegistrationSlider.vue index 478da0116..3a2d71db4 100644 --- a/webapp/components/Registration/RegistrationSlider.vue +++ b/webapp/components/Registration/RegistrationSlider.vue @@ -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), diff --git a/webapp/components/Select/LocationSelect.spec.js b/webapp/components/Select/LocationSelect.spec.js index a9af3b32b..14a1dbee3 100644 --- a/webapp/components/Select/LocationSelect.spec.js +++ b/webapp/components/Select/LocationSelect.spec.js @@ -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') + }) + }) }) }) diff --git a/webapp/components/Select/LocationSelect.vue b/webapp/components/Select/LocationSelect.vue index a9f0e75b7..9ea808aad 100644 --- a/webapp/components/Select/LocationSelect.vue +++ b/webapp/components/Select/LocationSelect.vue @@ -1,8 +1,7 @@ - {{ `${$t('settings.data.labelCity')}` }} - {{ `— ${locationName}` }} + {{ `${$t('settings.data.labelCity')}` + locationNameLabelAddOnOldName }} - +