Skip to content
This repository has been archived by the owner on Nov 23, 2024. It is now read-only.

Commit

Permalink
Merge pull request #581 from zendesk/ovillegas/save-language
Browse files Browse the repository at this point in the history
User language preference and user language menu
  • Loading branch information
samzx authored Jul 2, 2021
2 parents 510b18d + d1f097f commit 71ffea2
Show file tree
Hide file tree
Showing 24 changed files with 166 additions and 23 deletions.
13 changes: 13 additions & 0 deletions app/graphql/mutations/update_user_language_preference.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Mutations
class UpdateUserLanguagePreference < BaseMutation
description 'Sets user language preference'

null true

argument :id, ID, required: true

def resolve(**args)
UserPreferenceResolver.update_user_language_preference(object, args, context)
end
end
end
10 changes: 10 additions & 0 deletions app/graphql/resolvers/user_preference_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,15 @@ def confirm_profile_settings(context)

preference
end

def update_user_language_preference(_, args, context)
language_id = args[:id]
current_user_id = context[:current_user].id

user_preference = UserPreference.find_or_create_by(user_id: current_user_id)
user_preference.update(language_id: language_id)

{ id: language_id }
end
end
end
13 changes: 13 additions & 0 deletions app/graphql/types/language_graph_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Types
class LanguageGraphType < Types::BaseObject
graphql_name 'Language'
description 'Supported languages'

implements GraphQL::Relay::Node.interface
global_id_field :gid

field :id, ID, null: false
field :language_code, String, null: false
field :language_name, String, null: false
end
end
1 change: 1 addition & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ class MutationType < Types::BaseObject
field :delete_tag, TagGraphType, mutation: Mutations::DeleteTag

field :confirm_profile_settings, UserPreferenceGraphType, mutation: Mutations::ConfirmProfileSettings
field :update_user_language_preference, UserPreferenceGraphType, mutation: Mutations::UpdateUserLanguagePreference
end
end
5 changes: 5 additions & 0 deletions app/graphql/types/query_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,10 @@ def tag(id:)
def tags
Tag.all
end

field :languages, [LanguageGraphType], null: true
def languages
Language.all
end
end
end
1 change: 1 addition & 0 deletions app/graphql/types/user_preference_graph_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ class UserPreferenceGraphType < Types::BaseObject

field :id, ID, null: false
field :confirmed_profile_settings, Boolean, null: false
field :language_id, ID, null: false
end
end
5 changes: 3 additions & 2 deletions app/javascript/components/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const MuiTheme = {
},
}

const App = ({ loading, currentUser, offices, userPopover, toggleUserPopover, children }) => {
const App = ({ loading, currentUser, offices, userPopover, toggleUserPopover, children, languages }) => {
if (!currentUser) return null

return (
Expand All @@ -31,8 +31,9 @@ const App = ({ loading, currentUser, offices, userPopover, toggleUserPopover, ch
<div>
<Header>
<UserProfileMenu
languages={languages}
offices={offices}
togglePopover={e => toggleUserPopover(e.currentTarget)}
togglePopover={(e) => toggleUserPopover(e.currentTarget)}
popover={userPopover}
/>
</Header>
Expand Down
5 changes: 2 additions & 3 deletions app/javascript/components/UserProfileMenu/LanguageMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ const LanguageMenu = ( { languages, previousMenuValue, selectedItem, t }) =>
</PreviousItem>
<Separator />
{languages.map((language, i) => (
<Item key={i} value={{ office: selectedItem.office, language }}>
{' '}
{language.label}{' '}
<Item key={i} value={{ office: selectedItem.office, language: { label: language.languageName, value: language.languageCode } }}>
{language.languageName}
</Item>
))}
</>
Expand Down
33 changes: 23 additions & 10 deletions app/javascript/components/UserProfileMenu/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react'
import React, { useState, useContext, useEffect } from 'react'
import * as R from 'ramda'
import styled from 'styled-components'
import { withRouter } from 'react-router'
Expand All @@ -14,6 +14,7 @@ import { Avatar } from '@zendeskgarden/react-avatars'
import { MD } from '@zendeskgarden/react-typography'

import UpdateUserOfficeMutation from '/mutations/UpdateUserOfficeMutation.gql'
import UpdateUserLanguagePreferenceMutation from '/mutations/UpdateUserLanguagePreferenceMutation.gql'
import { UserContext, FilterContext } from '/context'
import GeneralSettingsMenu from './GeneralSettingsMenu'
import DefaultOfficeMenu from './DefaultOfficeMenu'
Expand All @@ -34,30 +35,38 @@ const UserName = styled(MD)`
font-weight: bold;
`

const UserProfileMenu = ({ offices, location, router, togglePopover }) => {
const languages = [
{ label: 'English', value: 'en' },
{ label: '日本語', value: 'ja' },
{ label: 'Español', value: 'es' },
]
const UserProfileMenu = ({ offices, location, router, togglePopover, languages }) => {

const { i18n, t } = useTranslation()
const { currentUser, setOffice } = useContext(UserContext)
const { setOfficeValue } = useContext(FilterContext)
const [ isOpen, setIsOpen ] = useState(false)
const [ tempSelectedItem, setTempSelectedItem ] = useState()
const [ selectedItem, setSelectedItem ] = useState({ office: currentUser.office, language: languages[0] })
const [ selectedItem, setSelectedItem ] = useState({ office: currentUser.office })
const [ updateDefaultOffice ] = useMutation(UpdateUserOfficeMutation)
const [ updateUserLanguagePreference ] = useMutation(UpdateUserLanguagePreferenceMutation)

if (R.isNil(currentUser) || R.isEmpty(currentUser)) return null


const getLanguageFromLanguageCode = (languageCode) => {
return R.find(R.propEq("languageCode", languageCode), languages)
}

// Syncs language menu with local language
useEffect(() => {
const selectedLanguage = getLanguageFromLanguageCode(i18n.language)
if (!!selectedLanguage) {
selectedLanguage && setSelectedItem({ ...selectedItem, language: { label: selectedLanguage.languageName, value: selectedLanguage.languageCode } })
}
}, [i18n.language])

const handleOfficeSelect = office =>
updateDefaultOffice({ variables: { userId: currentUser.id, officeId: office.id } })
.then(response => setOffice(R.path(['data', 'updateUserOffice', 'office'], response)))
.then(_ => setOfficeValue(office.id))
.then(_ => togglePopover('user'))


const handleStateChange = (changes, stateAndHelpers) => {
if (R.hasPath(['isOpen'])(changes)) {
setIsOpen(
Expand Down Expand Up @@ -95,7 +104,11 @@ const UserProfileMenu = ({ offices, location, router, togglePopover }) => {
i18n.changeLanguage(selectedItem.language.value, () => {
// TODO: Handle callback (error/success)
})
setSelectedItem(selectedItem)
const selectedLanguage = getLanguageFromLanguageCode(selectedItem.language.value)
if (!!selectedLanguage) {
updateUserLanguagePreference({ variables: { id: selectedLanguage.id } })
setSelectedItem(selectedItem)
}
}
// The following are for accessibility support for Link navigation
} else if (selectedItem === 'admin') {
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/fragments/LanguageEntry.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fragment LanguageEntry on Language {
id
languageCode
languageName
}
1 change: 1 addition & 0 deletions app/javascript/fragments/UserPreferenceEntry.gql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fragment UserPreferenceEntry on UserPreference {
id
confirmedProfileSettings
languageId
}
2 changes: 1 addition & 1 deletion app/javascript/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ i18n
.use(Backend)
.use(initReactI18next) // passes i18n down to react-i18next
.init({
lng: 'en',
lng: 'en', // reach out to local storage first.
fallbackLng: 'en',

keySeparator: false, // we do not use keys in form messages.welcome
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation updateUserLanguagePreference($id:ID!) {
updateUserLanguagePreference(id: $id) {
id
}
}
23 changes: 16 additions & 7 deletions app/javascript/pages/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ import { togglePopover } from 'actions'

import AppQuery from './query.gql'

// TODO refactor it into AppPage as a functional component
const MomentLocale = ({ children }) => {
const RemoteLocale = ({ children, currentUser, languages }) => {
const { i18n } = useTranslation()

useEffect(() => {
moment.locale(i18n.language)
}, [i18n.language])
const languageId = R.path(['preference', 'languageId'], currentUser)

if (!R.isNil(languageId)) {
const chosenLanguage = R.find(R.propEq('id', languageId))(languages)
const languageCode = R.prop('languageCode', chosenLanguage)
i18n.changeLanguage(languageCode)
moment.locale(languageCode)
}
}, [currentUser])

return <>{children}</>
}

Expand All @@ -28,15 +36,16 @@ class AppPage extends Component {

render() {
const {
data: { networkStatus, currentUser, offices },
data: { networkStatus, currentUser, offices, languages },
togglePopover,
popover,
children,
} = this.props

return (
<MomentLocale>
<RemoteLocale currentUser={currentUser} languages={languages}>
<App
languages={languages}
loading={networkStatus === NetworkStatus.loading}
currentUser={currentUser}
offices={offices}
Expand All @@ -45,7 +54,7 @@ class AppPage extends Component {
>
{children}
</App>
</MomentLocale>
</RemoteLocale>
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/pages/App/query.gql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "fragments/UserEntry.gql"
#import "fragments/OfficeEntry.gql"
#import "fragments/UserPreferenceEntry.gql"
#import "fragments/LanguageEntry.gql"

query AppQuery {
currentUser {
Expand All @@ -17,4 +18,7 @@ query AppQuery {
offices(sortBy: NAME_ASC) {
...OfficeEntry
}
languages {
...LanguageEntry
}
}
3 changes: 3 additions & 0 deletions app/models/language.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Language < ApplicationRecord
has_many :user_preferences, dependent: :nullify
end
1 change: 1 addition & 0 deletions app/models/user_preference.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class UserPreference < ApplicationRecord
belongs_to :user
belongs_to :language

scope :for_user, ->(user) { where(user: user) }
end
17 changes: 17 additions & 0 deletions db/migrate/20201216035051_create_language_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class CreateLanguageTable < ActiveRecord::Migration[5.2]
def change
create_table :languages do |t|
t.string :language_code
t.string :language_name
t.datetime :created_at, default: -> { 'CURRENT_TIMESTAMP' }
end

add_index :languages, :language_code, unique: true

return unless Language.all.length.zero?

[
['en', 'English'], ['es', 'Español'], ['ja', '日本語']
].each { |language| Language.create_with(language_code: language[0]).find_or_create_by(language_name: language[1]) }
end
end
6 changes: 6 additions & 0 deletions db/migrate/20201216035300_add_language_to_user_preference.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddLanguageToUserPreference < ActiveRecord::Migration[5.2]
def change
add_column :user_preferences, :language_id, :bigint, default: 1
add_foreign_key :user_preferences, :languages
end
end
9 changes: 9 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@
t.index ["user_id"], name: "index_individual_events_on_user_id"
end

create_table "languages", force: :cascade do |t|
t.string "language_code"
t.string "language_name"
t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }
t.index ["language_code"], name: "index_languages_on_language_code", unique: true
end

create_table "offices", id: :serial, force: :cascade do |t|
t.string "name", null: false
t.string "identifier", null: false
Expand Down Expand Up @@ -145,6 +152,7 @@
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "language_id", default: 1
t.index ["user_id"], name: "index_user_preferences_on_user_id"
end

Expand Down Expand Up @@ -184,5 +192,6 @@
add_foreign_key "event_tags", "tags"
add_foreign_key "individual_event_tags", "individual_events"
add_foreign_key "individual_event_tags", "tags"
add_foreign_key "user_preferences", "languages"
add_foreign_key "user_preferences", "users"
end
6 changes: 6 additions & 0 deletions test/fixtures/language.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

minimum:
id: 1
language_code: ts
language_name: Test
8 changes: 8 additions & 0 deletions test/graphql/resolvers/user_preference_resolver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,13 @@
assert_not_nil result
assert_equal true, result.confirmed_profile_settings
end

it 'should set update user language preference' do
context = { current_user: users(:a) }
args = { id: 1 }
result = UserPreferenceResolver.update_user_language_preference(nil, args, context)
assert_not_nil result
assert_equal 1, result[:id]
end
end
end
2 changes: 2 additions & 0 deletions test/integration/cleanliness_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
app/graphql/mutations/create_tag.rb
app/graphql/mutations/update_tag.rb
app/graphql/mutations/confirm_profile_settings.rb
app/graphql/mutations/update_user_language_preference.rb
app/graphql/portal_schema.rb
app/graphql/resolvers/office_resolver.rb
app/graphql/resolvers/organization_resolver.rb
Expand Down Expand Up @@ -89,6 +90,7 @@
app/graphql/types/user_graph_type.rb
app/graphql/types/tag_graph_type.rb
app/graphql/types/user_preference_graph_type.rb
app/graphql/types/language_graph_type.rb
app/helpers/application_helper.rb
]
)
Expand Down
11 changes: 11 additions & 0 deletions test/models/language_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require_relative '../test_helper'

SingleCov.covered!

describe Language do
fixtures :language
it 'creates a language' do
language = Language.new(language_code: 'en', language_name: 'English')
assert language.save
end
end

0 comments on commit 71ffea2

Please sign in to comment.