1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-07-15 23:54:29 +02:00

User updates (#3244)

* retrieve additional user fields

* implement config setting

* cleanup

* fix tests

* remove unused constant

* fix more tests

* add tests, cleanup

* lint fixes

* revert bad lint fixes

* more merge fixes

* update to use personal setting

* add user settings

* update package-lock.json

* lint fixes

* npm audit fix

* update tests

* Revert "lint fixes"

This reverts commit 6a50d335ca.

* Revert "update package-lock.json"

This reverts commit 1117e557e4.

* Revert "npm audit fix"

This reverts commit 77ea931c77.

* Revert "Revert "lint fixes""

This reverts commit 3ebd81b9fa.

* restore to original

* fix for empty prefs

* fix bad merge

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Scott Bishel
2022-07-08 08:43:43 -06:00
committed by GitHub
parent 06c1ae41a9
commit 22aae37dd1
38 changed files with 292 additions and 47 deletions

View File

@ -96,6 +96,7 @@ func (p *Plugin) OnConfigurationChange() error {
} }
p.server.Config().EnableDataRetention = enableBoardsDeletion p.server.Config().EnableDataRetention = enableBoardsDeletion
p.server.Config().DataRetentionDays = *mmconfig.DataRetentionSettings.BoardsRetentionDays p.server.Config().DataRetentionDays = *mmconfig.DataRetentionSettings.BoardsRetentionDays
p.server.Config().TeammateNameDisplay = *mmconfig.TeamSettings.TeammateNameDisplay
p.server.UpdateAppConfig() p.server.UpdateAppConfig()
p.wsPluginAdapter.BroadcastConfigChange(*p.server.App().GetClientConfig()) p.wsPluginAdapter.BroadcastConfigChange(*p.server.App().GetClientConfig())

View File

@ -62,11 +62,16 @@ func TestOnConfigurationChange(t *testing.T) {
baseDataRetentionSettings := &serverModel.DataRetentionSettings{ baseDataRetentionSettings := &serverModel.DataRetentionSettings{
BoardsRetentionDays: &intRef, BoardsRetentionDays: &intRef,
} }
usernameRef := "username"
baseTeamSettings := &serverModel.TeamSettings{
TeammateNameDisplay: &usernameRef,
}
baseConfig := &serverModel.Config{ baseConfig := &serverModel.Config{
FeatureFlags: baseFeatureFlags, FeatureFlags: baseFeatureFlags,
PluginSettings: *basePluginSettings, PluginSettings: *basePluginSettings,
DataRetentionSettings: *baseDataRetentionSettings, DataRetentionSettings: *baseDataRetentionSettings,
TeamSettings: *baseTeamSettings,
} }
t.Run("Test Load Plugin Success", func(t *testing.T) { t.Run("Test Load Plugin Success", func(t *testing.T) {

View File

@ -261,6 +261,7 @@ func (p *Plugin) createBoardsConfig(mmconfig mmModel.Config, baseURL string, ser
NotifyFreqBoardSeconds: getPluginSettingInt(mmconfig, notifyFreqBoardSecondsKey, 86400), NotifyFreqBoardSeconds: getPluginSettingInt(mmconfig, notifyFreqBoardSecondsKey, 86400),
EnableDataRetention: enableBoardsDeletion, EnableDataRetention: enableBoardsDeletion,
DataRetentionDays: *mmconfig.DataRetentionSettings.BoardsRetentionDays, DataRetentionDays: *mmconfig.DataRetentionSettings.BoardsRetentionDays,
TeammateNameDisplay: *mmconfig.TeamSettings.TeammateNameDisplay,
} }
} }

View File

@ -68,6 +68,10 @@ func TestSetConfiguration(t *testing.T) {
baseDataRetentionSettings := &model.DataRetentionSettings{ baseDataRetentionSettings := &model.DataRetentionSettings{
BoardsRetentionDays: &days, BoardsRetentionDays: &days,
} }
usernameRef := "username"
baseTeamSettings := &model.TeamSettings{
TeammateNameDisplay: &usernameRef,
}
baseConfig := &model.Config{ baseConfig := &model.Config{
FeatureFlags: baseFeatureFlags, FeatureFlags: baseFeatureFlags,
@ -75,6 +79,7 @@ func TestSetConfiguration(t *testing.T) {
SqlSettings: *baseSQLSettings, SqlSettings: *baseSQLSettings,
FileSettings: *baseFileSettings, FileSettings: *baseFileSettings,
DataRetentionSettings: *baseDataRetentionSettings, DataRetentionSettings: *baseDataRetentionSettings,
TeamSettings: *baseTeamSettings,
} }
t.Run("test enable telemetry", func(t *testing.T) { t.Run("test enable telemetry", func(t *testing.T) {

View File

@ -12,6 +12,8 @@ import {GlobalState} from 'mattermost-redux/types/store'
import {selectTeam} from 'mattermost-redux/actions/teams' import {selectTeam} from 'mattermost-redux/actions/teams'
import {SuiteWindow} from '../../../webapp/src/types/index' import {SuiteWindow} from '../../../webapp/src/types/index'
import {UserSettings} from '../../../webapp/src/userSettings'
const windowAny = (window as SuiteWindow) const windowAny = (window as SuiteWindow)
windowAny.baseURL = '/plugins/focalboard' windowAny.baseURL = '/plugins/focalboard'
@ -182,6 +184,7 @@ export default class Plugin {
this.registry = registry this.registry = registry
UserSettings.nameFormat = mmStore.getState().entities.preferences?.myPreferences['display_settings--name_format']?.value || null
let theme = mmStore.getState().entities.preferences.myPreferences.theme let theme = mmStore.getState().entities.preferences.myPreferences.theme
setMattermostTheme(theme) setMattermostTheme(theme)
let lastViewedChannel = mmStore.getState().entities.channels.currentChannelId let lastViewedChannel = mmStore.getState().entities.channels.currentChannelId
@ -326,6 +329,9 @@ export default class Plugin {
setMattermostTheme(JSON.parse(preference.value)) setMattermostTheme(JSON.parse(preference.value))
theme = preference.value theme = preference.value
} }
if(preference.category === 'display_settings' && preference.name === 'name_format'){
UserSettings.nameFormat = preference.value
}
} }
} }
}) })

View File

@ -1100,7 +1100,6 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return return
} }
jsonBytesResponse(w, http.StatusOK, userData) jsonBytesResponse(w, http.StatusOK, userData)
auditRec.AddMeta("userID", user.ID) auditRec.AddMeta("userID", user.ID)

View File

@ -9,6 +9,7 @@ func (a *App) GetClientConfig() *model.ClientConfig {
Telemetry: a.config.Telemetry, Telemetry: a.config.Telemetry,
TelemetryID: a.config.TelemetryID, TelemetryID: a.config.TelemetryID,
EnablePublicSharedBoards: a.config.EnablePublicSharedBoards, EnablePublicSharedBoards: a.config.EnablePublicSharedBoards,
TeammateNameDisplay: a.config.TeammateNameDisplay,
FeatureFlags: a.config.FeatureFlags, FeatureFlags: a.config.FeatureFlags,
} }
} }

View File

@ -19,6 +19,7 @@ func TestGetClientConfig(t *testing.T) {
newConfiguration.FeatureFlags = make(map[string]string) newConfiguration.FeatureFlags = make(map[string]string)
newConfiguration.FeatureFlags["BoardsFeature1"] = "true" newConfiguration.FeatureFlags["BoardsFeature1"] = "true"
newConfiguration.FeatureFlags["BoardsFeature2"] = "true" newConfiguration.FeatureFlags["BoardsFeature2"] = "true"
newConfiguration.TeammateNameDisplay = "username"
th.App.SetConfig(&newConfiguration) th.App.SetConfig(&newConfiguration)
clientConfig := th.App.GetClientConfig() clientConfig := th.App.GetClientConfig()
@ -26,5 +27,6 @@ func TestGetClientConfig(t *testing.T) {
require.True(t, clientConfig.Telemetry) require.True(t, clientConfig.Telemetry)
require.Equal(t, "abcde", clientConfig.TelemetryID) require.Equal(t, "abcde", clientConfig.TelemetryID)
require.Equal(t, 2, len(clientConfig.FeatureFlags)) require.Equal(t, 2, len(clientConfig.FeatureFlags))
require.Equal(t, "username", clientConfig.TeammateNameDisplay)
}) })
} }

View File

@ -15,6 +15,10 @@ type ClientConfig struct {
// required: true // required: true
EnablePublicSharedBoards bool `json:"enablePublicSharedBoards"` EnablePublicSharedBoards bool `json:"enablePublicSharedBoards"`
// Is public shared boards enabled
// required: true
TeammateNameDisplay string `json:"teammateNameDisplay"`
// The server feature flags // The server feature flags
// required: true // required: true
FeatureFlags map[string]string `json:"featureFlags"` FeatureFlags map[string]string `json:"featureFlags"`

View File

@ -26,6 +26,13 @@ type User struct {
// required: true // required: true
Email string `json:"-"` Email string `json:"-"`
// The user's nickname
Nickname string `json:"nickname"`
// The user's first name
FirstName string `json:"firstname"`
// The user's last name
LastName string `json:"lastname"`
// swagger:ignore // swagger:ignore
Password string `json:"-"` Password string `json:"-"`

View File

@ -52,6 +52,7 @@ type Configuration struct {
FeatureFlags map[string]string `json:"featureFlags" mapstructure:"featureFlags"` FeatureFlags map[string]string `json:"featureFlags" mapstructure:"featureFlags"`
EnableDataRetention bool `json:"enable_data_retention" mapstructure:"enable_data_retention"` EnableDataRetention bool `json:"enable_data_retention" mapstructure:"enable_data_retention"`
DataRetentionDays int `json:"data_retention_days" mapstructure:"data_retention_days"` DataRetentionDays int `json:"data_retention_days" mapstructure:"data_retention_days"`
TeammateNameDisplay string `json:"teammate_name_display" mapstructure:"teammateNameDisplay"`
AuthMode string `json:"authMode" mapstructure:"authMode"` AuthMode string `json:"authMode" mapstructure:"authMode"`
@ -100,6 +101,7 @@ func ReadConfigFile(configFilePath string) (*Configuration, error) {
viper.SetDefault("EnableDataRetention", false) viper.SetDefault("EnableDataRetention", false)
viper.SetDefault("DataRetentionDays", 365) // 1 year is default viper.SetDefault("DataRetentionDays", 365) // 1 year is default
viper.SetDefault("PrometheusAddress", "") viper.SetDefault("PrometheusAddress", "")
viper.SetDefault("TeammateNameDisplay", "username")
err := viper.ReadInConfig() // Find and read the config file err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file if err != nil { // Handle errors reading the config file

View File

@ -266,7 +266,7 @@ func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType {
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, error) { func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, error) {
query := s.getQueryBuilder(). query := s.getQueryBuilder().
Select("u.id", "u.username", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at", Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot"). "u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
From("Users as u"). From("Users as u").
Join("TeamMembers as tm ON tm.UserID = u.ID"). Join("TeamMembers as tm ON tm.UserID = u.ID").
@ -291,7 +291,7 @@ func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, erro
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) { func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
query := s.getQueryBuilder(). query := s.getQueryBuilder().
Select("u.id", "u.username", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at", Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot"). "u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
From("Users as u"). From("Users as u").
Join("TeamMembers as tm ON tm.UserID = u.id"). Join("TeamMembers as tm ON tm.UserID = u.id").
@ -332,6 +332,10 @@ func (s *MattermostAuthLayer) usersFromRows(rows *sql.Rows) ([]*model.User, erro
err := rows.Scan( err := rows.Scan(
&user.ID, &user.ID,
&user.Username, &user.Username,
&user.Email,
&user.Nickname,
&user.FirstName,
&user.LastName,
&propsBytes, &propsBytes,
&user.CreateAt, &user.CreateAt,
&user.UpdateAt, &user.UpdateAt,
@ -385,6 +389,9 @@ func mmUserToFbUser(mmUser *mmModel.User) model.User {
Username: mmUser.Username, Username: mmUser.Username,
Email: mmUser.Email, Email: mmUser.Email,
Password: mmUser.Password, Password: mmUser.Password,
Nickname: mmUser.Nickname,
FirstName: mmUser.FirstName,
LastName: mmUser.LastName,
MfaSecret: mmUser.MfaSecret, MfaSecret: mmUser.MfaSecret,
AuthService: mmUser.AuthService, AuthService: mmUser.AuthService,
AuthData: authData, AuthData: authData,

View File

@ -61,6 +61,9 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: {}, props: {},
create_at: 0, create_at: 0,
update_at: 0, update_at: 0,

View File

@ -96,12 +96,15 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
} }
const me: IUser = { const me: IUser = {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', nickname: '',
props: {}, firstname: '',
create_at: 0, lastname: '',
update_at: 0, email: '',
props: {},
create_at: 0,
update_at: 0,
is_bot: false, is_bot: false,
roles: 'system_user', roles: 'system_user',
} }

View File

@ -62,7 +62,10 @@ describe('components/content/TextElement', () => {
boards: { boards: {
[board1.id]: board1, [board1.id]: board1,
} }
} },
clientConfig: {
value: {},
},
} }
const store = mockStateStore([], state) const store = mockStateStore([], state)

View File

@ -83,7 +83,10 @@ describe('components/contentBlock', () => {
boards: { boards: {
[board1.id]: board1, [board1.id]: board1,
} }
} },
clientConfig: {
value: {},
},
} }
const store = mockStateStore([], state) const store = mockStateStore([], state)

View File

@ -37,7 +37,10 @@ describe('components/markdownEditor', () => {
boards: { boards: {
[board1.id]: board1, [board1.id]: board1,
} }
} },
clientConfig: {
value: {},
},
} }
const store = mockStateStore([], state) const store = mockStateStore([], state)
test('should match snapshot', async () => { test('should match snapshot', async () => {

View File

@ -27,6 +27,9 @@ const Entry = (props: EntryComponentProps): ReactElement => {
{mention.name} {mention.name}
{BotBadge && <BotBadge show={mention.is_bot}/>} {BotBadge && <BotBadge show={mention.is_bot}/>}
</div> </div>
<div className={theme?.mentionSuggestionsEntryText}>
{mention.displayName}
</div>
</div> </div>
</div> </div>
) )

View File

@ -21,9 +21,13 @@ import createLiveMarkdownPlugin from '../live-markdown-plugin/liveMarkdownPlugin
import './markdownEditorInput.scss' import './markdownEditorInput.scss'
import {BoardTypeOpen} from "../../blocks/board" import {BoardTypeOpen} from '../../blocks/board'
import {getCurrentBoard} from "../../store/boards" import {getCurrentBoard} from '../../store/boards'
import octoClient from "../../octoClient" import octoClient from '../../octoClient'
import {Utils} from '../../utils'
import {ClientConfig} from '../../config/clientConfig'
import {getClientConfig} from '../../store/clientConfig'
import Entry from './entryComponent/entryComponent' import Entry from './entryComponent/entryComponent'
@ -33,6 +37,7 @@ type MentionUser = {
name: string name: string
avatar: string avatar: string
is_bot: boolean is_bot: boolean
displayName: string
} }
type Props = { type Props = {
@ -48,6 +53,7 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
const {onChange, onFocus, onBlur, initialText, id, isEditing} = props const {onChange, onFocus, onBlur, initialText, id, isEditing} = props
const boardUsers = useAppSelector<IUser[]>(getBoardUsersList) const boardUsers = useAppSelector<IUser[]>(getBoardUsersList)
const board = useAppSelector(getCurrentBoard) const board = useAppSelector(getCurrentBoard)
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
const ref = useRef<Editor>(null) const ref = useRef<Editor>(null)
const [suggestions, setSuggestions] = useState<Array<MentionUser>>([]) const [suggestions, setSuggestions] = useState<Array<MentionUser>>([])
@ -65,7 +71,8 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
(user) => ({ (user) => ({
name: user.username, name: user.username,
avatar: `${imageURLForUser ? imageURLForUser(user.id) : ''}`, avatar: `${imageURLForUser ? imageURLForUser(user.id) : ''}`,
is_bot: user.is_bot} is_bot: user.is_bot,
displayName: Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
)) ))
setSuggestions(mentions) setSuggestions(mentions)
} }

View File

@ -41,6 +41,9 @@ describe('components/messages/CloudMessage', () => {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: {}, props: {},
create_at: 0, create_at: 0,
update_at: 0, update_at: 0,
@ -70,6 +73,9 @@ describe('components/messages/CloudMessage', () => {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: { props: {
focalboard_cloudMessageCanceled: 'true', focalboard_cloudMessageCanceled: 'true',
}, },
@ -101,6 +107,9 @@ describe('components/messages/CloudMessage', () => {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: {}, props: {},
create_at: 0, create_at: 0,
update_at: 0, update_at: 0,
@ -138,6 +147,9 @@ describe('components/messages/CloudMessage', () => {
id: 'single-user', id: 'single-user',
username: 'single-user', username: 'single-user',
email: 'single-user', email: 'single-user',
nickname: '',
firstname: '',
lastname: '',
props: {}, props: {},
create_at: 0, create_at: 0,
update_at: Date.now() - (1000 * 60 * 60 * 24), //24 hours, update_at: Date.now() - (1000 * 60 * 60 * 24), //24 hours,

View File

@ -24,6 +24,11 @@ describe('components/properties/createdBy', () => {
'user-id-1': {username: 'username_1'} as IUser, 'user-id-1': {username: 'username_1'} as IUser,
}, },
}, },
clientConfig: {
value: {
teammateNameDisplay: 'username',
},
},
}) })
const component = ( const component = (

View File

@ -42,6 +42,11 @@ describe('components/properties/lastModifiedBy', () => {
[card.id]: [comment], [card.id]: [comment],
}, },
}, },
clientConfig: {
value: {
teammateNameDisplay: 'username',
},
},
}) })
const component = ( const component = (

View File

@ -32,6 +32,11 @@ describe('components/properties/user', () => {
}, },
}, },
}, },
clientConfig: {
value: {
teammateNameDisplay: 'username',
},
},
} }
test('not readonly not existing user', async () => { test('not readonly not existing user', async () => {
@ -110,8 +115,7 @@ describe('components/properties/user', () => {
<UserProperty <UserProperty
value={'user-id-1'} value={'user-id-1'}
readonly={false} readonly={false}
onChange={() => { onChange={() => {}}
}}
/> />
</ReduxProvider>, </ReduxProvider>,
) )

View File

@ -5,12 +5,17 @@ import React from 'react'
import Select from 'react-select' import Select from 'react-select'
import {CSSObject} from '@emotion/serialize' import {CSSObject} from '@emotion/serialize'
import {Utils} from '../../../utils'
import {IUser} from '../../../user' import {IUser} from '../../../user'
import {getBoardUsersList, getBoardUsers} from '../../../store/users' import {getBoardUsersList, getBoardUsers} from '../../../store/users'
import {useAppSelector} from '../../../store/hooks' import {useAppSelector} from '../../../store/hooks'
import './user.scss' import './user.scss'
import {getSelectBaseStyle} from '../../../theme' import {getSelectBaseStyle} from '../../../theme'
import {ClientConfig} from '../../../config/clientConfig'
import {getClientConfig} from '../../../store/clientConfig'
import {propertyValueClassName} from '../../propertyValueUtils' import {propertyValueClassName} from '../../propertyValueUtils'
const imageURLForUser = (window as any).Components?.imageURLForUser const imageURLForUser = (window as any).Components?.imageURLForUser
@ -53,26 +58,28 @@ const selectStyles = {
}), }),
} }
const formatOptionLabel = (user: any) => { const UserProperty = (props: Props): JSX.Element => {
let profileImg const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
if (imageURLForUser) {
profileImg = imageURLForUser(user.id) const formatOptionLabel = (user: any) => {
let profileImg
if (imageURLForUser) {
profileImg = imageURLForUser(user.id)
}
return (
<div className='UserProperty-item'>
{profileImg && (
<img
alt='UserProperty-avatar'
src={profileImg}
/>
)}
{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
</div>
)
} }
return (
<div className='UserProperty-item'>
{profileImg && (
<img
alt='UserProperty-avatar'
src={profileImg}
/>
)}
{user.username}
</div>
)
}
const UserProperty = (props: Props): JSX.Element => {
const boardUsersById = useAppSelector<{[key:string]: IUser}>(getBoardUsers) const boardUsersById = useAppSelector<{[key:string]: IUser}>(getBoardUsers)
const user = boardUsersById[props.value] const user = boardUsersById[props.value]

View File

@ -101,6 +101,9 @@ const me: IUser = {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: {}, props: {},
create_at: 0, create_at: 0,
update_at: 0, update_at: 0,
@ -157,6 +160,7 @@ describe('src/components/shareBoard/shareBoard', () => {
telemetry: true, telemetry: true,
telemetryid: 'telemetry', telemetryid: 'telemetry',
enablePublicSharedBoards: true, enablePublicSharedBoards: true,
teammateNameDisplay: 'username',
featureFlags: {}, featureFlags: {},
}, },
}, },
@ -479,6 +483,7 @@ describe('src/components/shareBoard/shareBoard', () => {
} }
mockedOctoClient.getSharing.mockResolvedValue(sharing) mockedOctoClient.getSharing.mockResolvedValue(sharing)
mockedUtils.isFocalboardPlugin.mockReturnValue(true) mockedUtils.isFocalboardPlugin.mockReturnValue(true)
mockedUtils.getUserDisplayName.mockImplementation((u) => u.username)
const users:IUser[] = [ const users:IUser[] = [
{id: 'userid1', username: 'username_1'} as IUser, {id: 'userid1', username: 'username_1'} as IUser,

View File

@ -13,6 +13,9 @@ import {getCurrentBoard, getCurrentBoardMembers} from '../../store/boards'
import {Channel, ChannelTypeOpen, ChannelTypePrivate} from '../../store/channels' import {Channel, ChannelTypeOpen, ChannelTypePrivate} from '../../store/channels'
import {getMe, getBoardUsersList} from '../../store/users' import {getMe, getBoardUsersList} from '../../store/users'
import {ClientConfig} from '../../config/clientConfig'
import {getClientConfig} from '../../store/clientConfig'
import {Utils, IDType} from '../../utils' import {Utils, IDType} from '../../utils'
import Tooltip from '../../widgets/tooltip' import Tooltip from '../../widgets/tooltip'
import mutator from '../../mutator' import mutator from '../../mutator'
@ -100,6 +103,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
const [showLinkChannelConfirmation, setShowLinkChannelConfirmation] = useState<Channel|null>(null) const [showLinkChannelConfirmation, setShowLinkChannelConfirmation] = useState<Channel|null>(null)
const [sharing, setSharing] = useState<ISharing|undefined>(undefined) const [sharing, setSharing] = useState<ISharing|undefined>(undefined)
const [selectedUser, setSelectedUser] = useState<IUser|Channel|null>(null) const [selectedUser, setSelectedUser] = useState<IUser|Channel|null>(null)
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
// members of the current board // members of the current board
const members = useAppSelector<{[key: string]: BoardMember}>(getCurrentBoardMembers) const members = useAppSelector<{[key: string]: BoardMember}>(getCurrentBoardMembers)
@ -293,7 +297,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
/> />
} }
<div className='ml-3'> <div className='ml-3'>
<strong>{user.username}</strong> <strong>{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}</strong>
<strong className='ml-2 text-light'>{`@${user.username}`}</strong> <strong className='ml-2 text-light'>{`@${user.username}`}</strong>
</div> </div>
</div> </div>
@ -390,6 +394,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
key={user.id} key={user.id}
user={user} user={user}
member={members[user.id]} member={members[user.id]}
teammateNameDisplay={me?.props?.teammateNameDisplay || clientConfig.teammateNameDisplay}
onDeleteBoardMember={onDeleteBoardMember} onDeleteBoardMember={onDeleteBoardMember}
onUpdateBoardMember={onUpdateBoardMember} onUpdateBoardMember={onUpdateBoardMember}
isMe={user.id === me?.id} isMe={user.id === me?.id}

View File

@ -21,13 +21,14 @@ type Props = {
user: IUser user: IUser
member: BoardMember member: BoardMember
isMe: boolean isMe: boolean
teammateNameDisplay: string,
onDeleteBoardMember: (member: BoardMember) => void onDeleteBoardMember: (member: BoardMember) => void
onUpdateBoardMember: (member: BoardMember, permission: string) => void onUpdateBoardMember: (member: BoardMember, permission: string) => void
} }
const UserPermissionsRow = (props: Props): JSX.Element => { const UserPermissionsRow = (props: Props): JSX.Element => {
const intl = useIntl() const intl = useIntl()
const {user, member, isMe} = props const {user, member, isMe, teammateNameDisplay} = props
let currentRole = 'Viewer' let currentRole = 'Viewer'
if (member.schemeAdmin) { if (member.schemeAdmin) {
currentRole = 'Admin' currentRole = 'Admin'
@ -47,7 +48,7 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
/> />
} }
<div className='ml-3'> <div className='ml-3'>
<strong>{user.username}</strong> <strong>{Utils.getUserDisplayName(user, teammateNameDisplay)}</strong>
<strong className='ml-2 text-light'>{`@${user.username}`}</strong> <strong className='ml-2 text-light'>{`@${user.username}`}</strong>
{isMe && <strong className='ml-2 text-light'>{intl.formatMessage({id: 'ShareBoard.userPermissionsYouText', defaultMessage: '(You)'})}</strong>} {isMe && <strong className='ml-2 text-light'>{intl.formatMessage({id: 'ShareBoard.userPermissionsYouText', defaultMessage: '(You)'})}</strong>}
</div> </div>

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/table/Table extended should match snapshot with CreatedBy 1`] = ` exports[`components/table/Table extended should match snapshot with CreatedAt 1`] = `
<div> <div>
<div <div
class="Table" class="Table"
@ -460,7 +460,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1`
</div> </div>
`; `;
exports[`components/table/Table extended should match snapshot with CreatedBy 2`] = ` exports[`components/table/Table extended should match snapshot with CreatedBy 1`] = `
<div> <div>
<div <div
class="Table" class="Table"

View File

@ -301,9 +301,14 @@ describe('components/table/Table extended', () => {
board_id: {userId: 'user_id_1', schemeAdmin: true}, board_id: {userId: 'user_id_1', schemeAdmin: true},
}, },
}, },
clientConfig: {
value: {
teammateNameDisplay: 'username',
},
},
} }
test('should match snapshot with CreatedBy', async () => { test('should match snapshot with CreatedAt', async () => {
const board = TestBlockFactory.createBoard() const board = TestBlockFactory.createBoard()
const dateCreatedId = Utils.createGuid(IDType.User) const dateCreatedId = Utils.createGuid(IDType.User)

View File

@ -53,6 +53,9 @@ describe('components/viewTitle', () => {
[board.id]: {userId: 'user_id_1', schemeAdmin: true}, [board.id]: {userId: 'user_id_1', schemeAdmin: true},
}, },
}, },
clientConfig: {
value: {},
},
} }
const store = mockStateStore([], state) const store = mockStateStore([], state)

View File

@ -76,6 +76,9 @@ const me: IUser = {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: {}, props: {},
create_at: 0, create_at: 0,
update_at: 0, update_at: 0,
@ -150,6 +153,7 @@ describe('src/components/workspace', () => {
telemetry: true, telemetry: true,
telemetryid: 'telemetry', telemetryid: 'telemetry',
enablePublicSharedBoards: true, enablePublicSharedBoards: true,
teammateNameDisplay: 'username',
featureFlags: {}, featureFlags: {},
}, },
}, },
@ -272,6 +276,7 @@ describe('src/components/workspace', () => {
telemetry: true, telemetry: true,
telemetryid: 'telemetry', telemetryid: 'telemetry',
enablePublicSharedBoards: true, enablePublicSharedBoards: true,
teammateNameDisplay: 'username',
featureFlags: {}, featureFlags: {},
}, },
}, },
@ -310,6 +315,9 @@ describe('src/components/workspace', () => {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: { props: {
focalboard_welcomePageViewed: '1', focalboard_welcomePageViewed: '1',
focalboard_onboardingTourStarted: true, focalboard_onboardingTourStarted: true,
@ -361,6 +369,7 @@ describe('src/components/workspace', () => {
telemetry: true, telemetry: true,
telemetryid: 'telemetry', telemetryid: 'telemetry',
enablePublicSharedBoards: true, enablePublicSharedBoards: true,
teammateNameDisplay: 'username',
featureFlags: {}, featureFlags: {},
}, },
}, },
@ -410,6 +419,9 @@ describe('src/components/workspace', () => {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: { props: {
focalboard_welcomePageViewed: '1', focalboard_welcomePageViewed: '1',
focalboard_onboardingTourStarted: true, focalboard_onboardingTourStarted: true,
@ -461,6 +473,7 @@ describe('src/components/workspace', () => {
telemetry: true, telemetry: true,
telemetryid: 'telemetry', telemetryid: 'telemetry',
enablePublicSharedBoards: true, enablePublicSharedBoards: true,
teammateNameDisplay: 'username',
featureFlags: {}, featureFlags: {},
}, },
}, },
@ -515,6 +528,9 @@ describe('src/components/workspace', () => {
id: 'user-id-1', id: 'user-id-1',
username: 'username_1', username: 'username_1',
email: '', email: '',
nickname: '',
firstname: '',
lastname: '',
props: { props: {
focalboard_welcomePageViewed: '1', focalboard_welcomePageViewed: '1',
focalboard_onboardingTourStarted: true, focalboard_onboardingTourStarted: true,
@ -566,6 +582,7 @@ describe('src/components/workspace', () => {
telemetry: true, telemetry: true,
telemetryid: 'telemetry', telemetryid: 'telemetry',
enablePublicSharedBoards: true, enablePublicSharedBoards: true,
teammateNameDisplay: 'username',
featureFlags: {}, featureFlags: {},
}, },
}, },

View File

@ -6,4 +6,5 @@ export type ClientConfig = {
telemetryid: string, telemetryid: string,
enablePublicSharedBoards: boolean, enablePublicSharedBoards: boolean,
featureFlags: Record<string, string>, featureFlags: Record<string, string>,
teammateNameDisplay: string,
} }

View File

@ -7,6 +7,8 @@ import {ClientConfig} from '../config/clientConfig'
import {default as client} from '../octoClient' import {default as client} from '../octoClient'
import {ShowUsername} from '../utils'
import {RootState} from './index' import {RootState} from './index'
export const fetchClientConfig = createAsyncThunk( export const fetchClientConfig = createAsyncThunk(
@ -16,7 +18,7 @@ export const fetchClientConfig = createAsyncThunk(
const clientConfigSlice = createSlice({ const clientConfigSlice = createSlice({
name: 'config', name: 'config',
initialState: {value: {telemetry: false, telemetryid: '', enablePublicSharedBoards: false, featureFlags: {}}} as {value: ClientConfig}, initialState: {value: {telemetry: false, telemetryid: '', enablePublicSharedBoards: false, teammateNameDisplay: ShowUsername, featureFlags: {}}} as {value: ClientConfig},
reducers: { reducers: {
setClientConfig: (state, action: PayloadAction<ClientConfig>) => { setClientConfig: (state, action: PayloadAction<ClientConfig>) => {
state.value = action.payload state.value = action.payload
@ -24,7 +26,7 @@ const clientConfigSlice = createSlice({
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(fetchClientConfig.fulfilled, (state, action) => { builder.addCase(fetchClientConfig.fulfilled, (state, action) => {
state.value = action.payload || {telemetry: false, telemetryid: '', enablePublicSharedBoards: false, featureFlags: {}} state.value = action.payload || {telemetry: false, telemetryid: '', enablePublicSharedBoards: false, teammateNameDisplay: ShowUsername, featureFlags: {}}
}) })
}, },
}) })
@ -35,4 +37,3 @@ export const {reducer} = clientConfigSlice
export function getClientConfig(state: RootState): ClientConfig { export function getClientConfig(state: RootState): ClientConfig {
return state.clientConfig.value return state.clientConfig.value
} }

View File

@ -187,6 +187,9 @@ class TestBlockFactory {
id: 'user-id-1', id: 'user-id-1',
username: 'Dwight Schrute', username: 'Dwight Schrute',
email: 'dwight.schrute@dundermifflin.com', email: 'dwight.schrute@dundermifflin.com',
nickname: '',
firstname: '',
lastname: '',
props: {}, props: {},
create_at: Date.now(), create_at: Date.now(),
update_at: Date.now(), update_at: Date.now(),

View File

@ -5,6 +5,9 @@ interface IUser {
id: string, id: string,
username: string, username: string,
email: string, email: string,
nickname: string,
firstname: string,
lastname: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
props: Record<string, any>, props: Record<string, any>,
create_at: number, create_at: number,

View File

@ -17,7 +17,8 @@ export enum UserSettingKey {
RandomIcons = 'randomIcons', RandomIcons = 'randomIcons',
MobileWarningClosed = 'mobileWarningClosed', MobileWarningClosed = 'mobileWarningClosed',
WelcomePageViewed = 'welcomePageViewed', WelcomePageViewed = 'welcomePageViewed',
HideCloudMessage = 'hideCloudMessage' HideCloudMessage = 'hideCloudMessage',
NameFormat = 'nameFormat'
} }
export class UserSettings { export class UserSettings {
@ -155,6 +156,15 @@ export class UserSettings {
static set hideCloudMessage(newValue: boolean) { static set hideCloudMessage(newValue: boolean) {
localStorage.setItem(UserSettingKey.HideCloudMessage, JSON.stringify(newValue)) localStorage.setItem(UserSettingKey.HideCloudMessage, JSON.stringify(newValue))
} }
static get nameFormat(): string | null {
return UserSettings.get(UserSettingKey.NameFormat)
}
static set nameFormat(newValue: string | null) {
UserSettings.set(UserSettingKey.NameFormat, newValue)
}
} }
export function exportUserSettingsBlob(): string { export function exportUserSettingsBlob(): string {

View File

@ -7,7 +7,9 @@ import {createMemoryHistory} from "history"
import {match as routerMatch} from "react-router-dom" import {match as routerMatch} from "react-router-dom"
import {Utils, IDType} from './utils' import {Utils, IDType, ShowFullName, ShowNicknameFullName, ShowUsername} from './utils'
import {IUser} from './user'
import {IAppWindow} from './types' import {IAppWindow} from './types'
declare let window: IAppWindow declare let window: IAppWindow
@ -186,4 +188,49 @@ describe('utils', () => {
expect(history.push).toBeCalledWith('/team/team_id_1/board_id_2') expect(history.push).toBeCalledWith('/team/team_id_1/board_id_2')
}) })
}) })
describe('getUserDisplayName test', () => {
const user: IUser = {
id: 'user-id-1',
username: 'username_1',
email: 'test@email.com',
nickname: 'nickname',
firstname: 'firstname',
lastname: 'lastname',
props: {},
create_at: 0,
update_at: 0,
is_bot: false,
roles: 'system_user',
}
it('should display username, by default', () => {
const displayName = Utils.getUserDisplayName(user, '')
expect(displayName).toEqual('username_1')
})
it('should display nickname', () => {
const displayName = Utils.getUserDisplayName(user, ShowNicknameFullName)
expect(displayName).toEqual('nickname')
})
it('should display fullname', () => {
const displayName = Utils.getUserDisplayName(user, ShowFullName)
expect(displayName).toEqual('firstname lastname')
})
it('should display username', () => {
const displayName = Utils.getUserDisplayName(user, ShowUsername)
expect(displayName).toEqual('username_1')
})
it('should display full name, no nickname', () => {
user.nickname = ''
const displayName = Utils.getUserDisplayName(user, ShowNicknameFullName)
expect(displayName).toEqual('firstname lastname')
})
it('should display username, no nickname, no full name', () => {
user.nickname = ''
user.firstname = ''
user.lastname = ''
const displayName = Utils.getUserDisplayName(user, ShowNicknameFullName)
expect(displayName).toEqual('username_1')
})
})
}) })

View File

@ -8,6 +8,8 @@ import {generatePath, match as routerMatch} from "react-router-dom"
import {History} from "history" import {History} from "history"
import {IUser} from './user'
import {Block} from './blocks/block' import {Block} from './blocks/block'
import {Board as BoardType, BoardMember, createBoard} from './blocks/board' import {Board as BoardType, BoardMember, createBoard} from './blocks/board'
import {createBoardView} from './blocks/boardView' import {createBoardView} from './blocks/boardView'
@ -16,6 +18,7 @@ import {createCommentBlock} from './blocks/commentBlock'
import {IAppWindow} from './types' import {IAppWindow} from './types'
import {ChangeHandlerType, WSMessage} from './wsclient' import {ChangeHandlerType, WSMessage} from './wsclient'
import {BoardCategoryWebsocketData, Category} from './store/sidebar' import {BoardCategoryWebsocketData, Category} from './store/sidebar'
import {UserSettings} from './userSettings'
declare let window: IAppWindow declare let window: IAppWindow
@ -49,6 +52,10 @@ export const KeyCodes: Record<string, [string, number]> = {
COMPOSING: ['Composing', 229], COMPOSING: ['Composing', 229],
} }
export const ShowUsername = 'username'
export const ShowNicknameFullName = 'nickname_full_name'
export const ShowFullName = 'full_name'
class Utils { class Utils {
static createGuid(idType: IDType): string { static createGuid(idType: IDType): string {
const data = Utils.randomArray(16) const data = Utils.randomArray(16)
@ -80,6 +87,45 @@ class Utils {
return imageURLForUser && userId ? imageURLForUser(userId) : defaultImageUrl return imageURLForUser && userId ? imageURLForUser(userId) : defaultImageUrl
} }
static getUserDisplayName(user: IUser, configNameFormat: string): string {
let nameFormat = configNameFormat
if(UserSettings.nameFormat){
nameFormat=UserSettings.nameFormat
}
// default nameFormat = 'username'
let displayName = user.username
if (nameFormat === ShowNicknameFullName) {
if( user.nickname != '') {
displayName = user.nickname
} else {
const fullName = Utils.getFullName(user)
if(fullName != ''){
displayName = fullName
}
}
} else if (nameFormat == ShowFullName) {
const fullName = Utils.getFullName(user)
if(fullName != ''){
displayName = fullName
}
}
return displayName
}
static getFullName(user: IUser): string {
if (user.firstname != '' && user.lastname != '') {
return user.firstname + ' ' + user.lastname
} else if (user.firstname != '') {
return user.firstname
} else if (user.lastname != '') {
return user.lastname
} else {
return ''
}
}
static randomArray(size: number): Uint8Array { static randomArray(size: number): Uint8Array {
const crypto = window.crypto || window.msCrypto const crypto = window.crypto || window.msCrypto
const rands = new Uint8Array(size) const rands = new Uint8Array(size)