diff --git a/NOTICE.txt b/NOTICE.txt index 3b8efdbc..181193d9 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1734,36 +1734,3 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---- - -## deepmerge - -This product contains 'deepmerge', a library for deep merging of JavaScript objects, by Kyle Mathews. - -* HOMEPAGE: - * https://github.com/KyleAMathews/deepmerge - -* LICENSE: - -The MIT License (MIT) - -Copyright (c) 2012 Nicholas Fisher - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/package.json b/package.json index 5ebca98c..100aed83 100644 --- a/package.json +++ b/package.json @@ -27,15 +27,15 @@ "serve": "gulp watch", "test": "npm-run-all test:* lint:*", "test:app": "npm run build && mocha --reporter mocha-circleci-reporter --recursive test/specs", - "package:all": "npm-run-all package:windows package:mac package:linux", - "package:windows": "npm run build && build --win --x64 --ia32 --em.name=mattermost --publish=never && npm run manipulate-windows-zip", - "package:mac": "npm run build && build --mac --publish=never", - "package:linux": "npm run build && build --linux --x64 --ia32 --em.name=mattermost-desktop --publish=never", + "package:all": "npm-run-all check-build-config package:windows package:mac package:linux", + "package:windows": "npm-run-all check-build-config build && build --win --x64 --ia32 --em.name=mattermost --publish=never && npm run manipulate-windows-zip", + "package:mac": "npm-run-all check-build-config build && build --mac --publish=never", + "package:linux": "npm-run-all check-build-config build && build --linux --x64 --ia32 --em.name=mattermost-desktop --publish=never", "manipulate-windows-zip": "node scripts/manipulate_windows_zip.js", - "lint:js": "eslint --ext .js --ext .jsx ." + "lint:js": "eslint --ext .js --ext .jsx .", + "check-build-config": "node scripts/check_build_config.js" }, "devDependencies": { - "deepmerge": "^1.5.2", "7zip-bin": "^2.0.4", "babel-core": "^6.24.1", "babel-eslint": "^7.2.3", diff --git a/scripts/check_build_config.js b/scripts/check_build_config.js new file mode 100644 index 00000000..4f18b543 --- /dev/null +++ b/scripts/check_build_config.js @@ -0,0 +1,16 @@ +const buildConfig = require('../src/common/config/buildConfig'); + +function validateBuildConfig(config) { + if (config.enableServerManagement === false && config.defaultTeams && config.defaultTeams.length === 0) { + return { + result: false, + message: `Specify at least one server for "defaultTeams" in buildConfig.js when "enableServerManagement is set to false.\n${JSON.stringify(config, null, 2)}` + }; + } + return {result: true}; +} + +const ret = validateBuildConfig(buildConfig); +if (ret.result === false) { + throw new Error(ret.message); +} diff --git a/src/browser/components/SettingsPage.jsx b/src/browser/components/SettingsPage.jsx index 95d7c5b1..9d4d6ac2 100644 --- a/src/browser/components/SettingsPage.jsx +++ b/src/browser/components/SettingsPage.jsx @@ -8,6 +8,7 @@ const {ipcRenderer, remote} = require('electron'); const AutoLaunch = require('auto-launch'); const {debounce} = require('underscore'); +const buildConfig = require('../../common/config/buildConfig'); const settings = require('../../common/settings'); const TeamList = require('./TeamList.jsx'); @@ -29,7 +30,8 @@ const CONFIG_TYPE_APP_OPTIONS = 'appOptions'; const SettingsPage = createReactClass({ propTypes: { - configFile: PropTypes.string + configFile: PropTypes.string, + enableServerManagement: PropTypes.bool }, getInitialState() { @@ -301,8 +303,10 @@ const SettingsPage = createReactClass({ onTeamsChange={this.handleTeamsChange} updateTeam={this.updateTeam} addServer={this.addServer} - onTeamClick={backToIndex} allowTeamEdit={this.state.enableTeamModification} + onTeamClick={(index) => { + backToIndex(index + buildConfig.defaultTeams.length); + }} /> @@ -339,7 +343,7 @@ const SettingsPage = createReactClass({ ); var srvMgmt; - if (this.state.enableServerManagement || this.state.teams.length === 0) { + if (this.props.enableServerManagement === true) { srvMgmt = (
{serversRow} @@ -514,7 +518,7 @@ const SettingsPage = createReactClass({ bsStyle='link' style={settingsPage.close} onClick={this.handleCancel} - disabled={this.state.teams.length === 0} + disabled={settings.mergeDefaultTeams(this.state.teams).length === 0} > {'×'} diff --git a/src/browser/index.jsx b/src/browser/index.jsx index ea2b2739..df1ac2a8 100644 --- a/src/browser/index.jsx +++ b/src/browser/index.jsx @@ -12,14 +12,18 @@ const {remote, ipcRenderer} = require('electron'); const MainPage = require('./components/MainPage.jsx'); const AppConfig = require('./config/AppConfig.js'); +const buildConfig = require('../common/config/buildConfig'); +const settings = require('../common/settings'); const url = require('url'); const badge = require('./js/badge'); const utils = require('../utils/util'); +const teams = settings.mergeDefaultTeams(AppConfig.data.teams); + remote.getCurrentWindow().removeAllListeners('focus'); -if (AppConfig.data.teams.length === 0) { +if (teams.length === 0) { window.location = 'settings.html'; } @@ -90,8 +94,9 @@ function showUnreadBadge(unreadCount, mentionCount) { const permissionRequestQueue = []; const requestingPermission = new Array(AppConfig.data.teams.length); -function teamConfigChange(teams) { - AppConfig.set('teams', teams); +function teamConfigChange(updatedTeams) { + AppConfig.set('teams', updatedTeams.slice(buildConfig.defaultTeams.length)); + teams.splice(0, teams.length, ...updatedTeams); requestingPermission.length = teams.length; ipcRenderer.send('update-menu', AppConfig.data); ipcRenderer.send('update-config'); @@ -157,14 +162,14 @@ if (!parsedURL.query.index || parsedURL.query.index === null) { ReactDOM.render( , diff --git a/src/browser/settings.jsx b/src/browser/settings.jsx index 305a4a06..1f463bf6 100644 --- a/src/browser/settings.jsx +++ b/src/browser/settings.jsx @@ -9,13 +9,17 @@ const React = require('react'); const ReactDOM = require('react-dom'); const SettingsPage = require('./components/SettingsPage.jsx'); const contextMenu = require('./js/contextMenu'); +const buildConfig = require('../common/config/buildConfig'); const configFile = remote.app.getPath('userData') + '/config.json'; contextMenu.setup(remote.getCurrentWindow()); ReactDOM.render( - , + , document.getElementById('content') ); diff --git a/src/common/config/base.json b/src/common/config/base.json deleted file mode 100644 index 6dca7463..00000000 --- a/src/common/config/base.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "default": { - "teams": [], - "showTrayIcon": false, - "trayIconTheme": "light", - "minimizeToTray": false, - "version": 1, - "notifications": { - "flashWindow": 0 - }, - "showUnreadBadge": true, - "useSpellChecker": true, - "spellCheckerLocale": "en-US", - "helpLink": "https://docs.mattermost.com/help/apps/desktop-guide.html", - "enableServerManagement": true - }, - "1": { - "teams": [], - "showTrayIcon": false, - "trayIconTheme": "light", - "minimizeToTray": false, - "version": 1, - "notifications": { - "flashWindow": 0 - }, - "showUnreadBadge": true, - "useSpellChecker": true, - "spellCheckerLocale": "en-US", - "helpLink": "https://docs.mattermost.com/help/apps/desktop-guide.html", - "enableServerManagement": true - } -} diff --git a/src/common/config/buildConfig.js b/src/common/config/buildConfig.js new file mode 100644 index 00000000..62ca0d54 --- /dev/null +++ b/src/common/config/buildConfig.js @@ -0,0 +1,23 @@ +/** + * Build-time configuration. End-users can't change these parameters. + * @prop {Object[]} defaultTeams + * @prop {string} defaultTeams[].name - The tab name for default team. + * @prop {string} defaultTeams[].url - The URL for default team. + * @prop {string} helpLink - The URL for "Help->Learn More..." menu item. + * If null is specified, the menu disappears. + * @prop {boolean} enableServerManagement - Whether users can edit servers configuration. + * Specify at least one server for "defaultTeams" + * when "enableServerManagement is set to false + */ +const buildConfig = { + defaultTeams: [/* + { + name: 'example', + url: 'https://example.com' + }*/ + ], + helpLink: 'https://docs.mattermost.com/help/apps/desktop-guide.html', + enableServerManagement: true +}; + +module.exports = buildConfig; diff --git a/src/common/config/defaultPreferences.js b/src/common/config/defaultPreferences.js new file mode 100644 index 00000000..39f96219 --- /dev/null +++ b/src/common/config/defaultPreferences.js @@ -0,0 +1,19 @@ +/** + * Default user preferences. End-users can change these parameters by editing config.json + * @param {number} version - Scheme version. (Not application version) + */ +const defaultPreferences = { + version: 1, + teams: [], + showTrayIcon: false, + trayIconTheme: 'light', + minimizeToTray: false, + notifications: { + flashWindow: 0 + }, + showUnreadBadge: true, + useSpellChecker: true, + spellCheckerLocale: 'en-US' +}; + +module.exports = defaultPreferences; diff --git a/src/common/config/override.json b/src/common/config/override.json deleted file mode 100644 index 0db3279e..00000000 --- a/src/common/config/override.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/src/common/config/pastDefaultPreferences.js b/src/common/config/pastDefaultPreferences.js new file mode 100644 index 00000000..5572c70d --- /dev/null +++ b/src/common/config/pastDefaultPreferences.js @@ -0,0 +1,11 @@ +const defaultPreferences = require('./defaultPreferences'); + +const pastDefaultPreferences = { + 0: { + url: '' + } +}; + +pastDefaultPreferences[`${defaultPreferences.version}`] = defaultPreferences; + +module.exports = pastDefaultPreferences; diff --git a/src/common/config/upgradePreferences.js b/src/common/config/upgradePreferences.js new file mode 100644 index 00000000..3e8485a3 --- /dev/null +++ b/src/common/config/upgradePreferences.js @@ -0,0 +1,29 @@ +const pastDefaultPreferences = require('./pastDefaultPreferences'); + +function deepCopy(object) { + return JSON.parse(JSON.stringify(object)); +} + +function upgradeV0toV1(configV0) { + const config = deepCopy(pastDefaultPreferences['1']); + if (config.version !== 1) { + throw new Error('pastDefaultPreferences[\'1\'].version is not equal to 1'); + } + config.teams.push({ + name: 'Primary team', + url: configV0.url + }); + return config; +} + +function upgradeToLatest(config) { + var configVersion = config.version ? config.version : 0; + switch (configVersion) { + case 0: + return upgradeToLatest(upgradeV0toV1(config)); + default: + return config; + } +} + +module.exports = upgradeToLatest; diff --git a/src/common/settings.js b/src/common/settings.js index b0ec9116..b435d504 100644 --- a/src/common/settings.js +++ b/src/common/settings.js @@ -1,90 +1,56 @@ 'use strict'; const fs = require('fs'); - const path = require('path'); -const deepmerge = require('./deepmerge'); - -const settingsVersion = 1; -const baseConfig = require('./config/base.json'); -const overrideConfig = require('./config/override.json'); +const buildConfig = require('./config/buildConfig'); function merge(base, target) { return Object.assign({}, base, target); } -function deepMergeArray(source, dest) { - return dest; -} +const defaultPreferences = require('./config/defaultPreferences'); +const upgradePreferences = require('./config/upgradePreferences'); -function loadDefault(version, spellCheckerLocale) { - var ver = version; - if (version == null) { - ver = settingsVersion; - } - - const base = baseConfig[ver] || baseConfig.default; - const override = overrideConfig[ver] || {}; - - const defaults = deepmerge(base, override, {arrayMerge: deepMergeArray}); - - return Object.assign(defaults, { - spellCheckerLocale: spellCheckerLocale || defaults.spellCheckerLocale || 'en-US' +function loadDefault(spellCheckerLocale) { + const config = JSON.parse(JSON.stringify(defaultPreferences)); + return Object.assign({}, config, { + spellCheckerLocale: spellCheckerLocale || defaultPreferences.pellCheckerLocale || 'en-US' }); } -function upgradeV0toV1(configV0) { - var config = loadDefault(1); - config.teams.push({ - name: 'Primary team', - url: configV0.url - }); - return config; +function hasBuildConfigDefaultTeams(config) { + return config.defaultTeams.length > 0; } -function upgrade(config, newAppVersion) { - var configVersion = config.version ? config.version : 0; - if (newAppVersion) { - config.lastMattermostVersion = newAppVersion; - } - switch (configVersion) { - case 0: - return upgrade(upgradeV0toV1(config)); - default: - return config; - } +function upgrade(config) { + return upgradePreferences(config); } module.exports = { - version: settingsVersion, + version: defaultPreferences.version, upgrade, readFileSync(configFile) { - var config = JSON.parse(fs.readFileSync(configFile, 'utf8')); - - // need to be able to compare 1 to '1' - if (config.version == settingsVersion) { // eslint-disable-line - var defaultConfig = this.loadDefault(); - config = merge(defaultConfig, config); + const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); + if (config.version === defaultPreferences.version) { + const defaultConfig = loadDefault(); + return merge(defaultConfig, config); } - return config; }, writeFile(configFile, config, callback) { - // need to be able to compare 1 to '1' - if (config.version != settingsVersion) { // eslint-disable-line - throw new Error('version ' + config.version + ' is not equal to ' + settingsVersion); + if (config.version !== defaultPreferences.version) { + throw new Error('version ' + config.version + ' is not equal to ' + defaultPreferences.version); } var data = JSON.stringify(config, null, ' '); fs.writeFile(configFile, data, 'utf8', callback); }, writeFileSync(configFile, config) { - // need to be able to compare 1 to '1' - if (config.version != settingsVersion) { // eslint-disable-line - throw new Error('version ' + config.version + ' is not equal to ' + settingsVersion); + if (config.version !== defaultPreferences.version) { + throw new Error('version ' + config.version + ' is not equal to ' + defaultPreferences.version); } const dir = path.dirname(configFile); @@ -96,5 +62,16 @@ module.exports = { fs.writeFileSync(configFile, data, 'utf8'); }, - loadDefault + loadDefault, + + mergeDefaultTeams(teams) { + const newTeams = []; + if (hasBuildConfigDefaultTeams(buildConfig)) { + newTeams.push(...JSON.parse(JSON.stringify(buildConfig.defaultTeams))); + } + if (buildConfig.enableServerManagement) { + newTeams.push(...JSON.parse(JSON.stringify(teams))); + } + return newTeams; + } }; diff --git a/src/main.js b/src/main.js index e59ffc7d..e8feed53 100644 --- a/src/main.js +++ b/src/main.js @@ -69,7 +69,7 @@ try { config = settings.readFileSync(configFile); if (config.version !== settings.version || wasUpdated()) { clearAppCache(); - config = settings.upgrade(config, app.getVersion()); + config = settings.upgrade(config); settings.writeFileSync(configFile, config); } } catch (e) { diff --git a/src/main/menus/app.js b/src/main/menus/app.js index 587bb465..9538b06c 100644 --- a/src/main/menus/app.js +++ b/src/main/menus/app.js @@ -1,6 +1,8 @@ 'use strict'; const electron = require('electron'); +const settings = require('../../common/settings'); +const buildConfig = require('../../common/config/buildConfig'); const Menu = electron.Menu; @@ -38,7 +40,7 @@ function createTemplate(mainWindow, config, isDev) { } }]; - if (config.enableServerManagement === true || config.teams.length === 0) { + if (buildConfig.enableServerManagement === true) { platformAppMenu.push({ label: 'Sign in to Another Server', click() { @@ -173,13 +175,14 @@ function createTemplate(mainWindow, config, isDev) { }] }); + const teams = settings.mergeDefaultTeams(config.teams); const windowMenu = { label: '&Window', submenu: [{ role: 'minimize' }, { role: 'close' - }, separatorItem, ...config.teams.slice(0, 9).map((team, i) => { + }, separatorItem, ...teams.slice(0, 9).map((team, i) => { return { label: team.name, accelerator: `CmdOrCtrl+${i + 1}`, @@ -194,23 +197,23 @@ function createTemplate(mainWindow, config, isDev) { click() { mainWindow.webContents.send('select-next-tab'); }, - enabled: (config.teams.length > 1) + enabled: (teams.length > 1) }, { label: 'Select Previous Server', accelerator: 'Ctrl+Shift+Tab', click() { mainWindow.webContents.send('select-previous-tab'); }, - enabled: (config.teams.length > 1) + enabled: (teams.length > 1) }] }; template.push(windowMenu); var submenu = []; - if (config.helpLink) { + if (buildConfig.helpLink) { submenu.push({ label: 'Learn More...', click() { - electron.shell.openExternal(config.helpLink); + electron.shell.openExternal(buildConfig.helpLink); } }); submenu.push(separatorItem); diff --git a/src/main/menus/tray.js b/src/main/menus/tray.js index 1de7615f..8db5587e 100644 --- a/src/main/menus/tray.js +++ b/src/main/menus/tray.js @@ -4,11 +4,13 @@ const { app, Menu } = require('electron'); +const settings = require('../../common/settings'); function createTemplate(mainWindow, config, isDev) { const settingsURL = isDev ? 'http://localhost:8080/browser/settings.html' : `file://${app.getAppPath()}/browser/settings.html`; + const teams = settings.mergeDefaultTeams(config.teams); var template = [ - ...config.teams.slice(0, 9).map((team, i) => { + ...teams.slice(0, 9).map((team, i) => { return { label: team.name, click: () => { diff --git a/src/package.json b/src/package.json index e6b0aad3..5f44f82f 100644 --- a/src/package.json +++ b/src/package.json @@ -12,7 +12,6 @@ "auto-launch": "^5.0.1", "bootstrap": "^3.3.7", "create-react-class": "^15.6.2", - "deepmerge": "^1.5.2", "electron-context-menu": "^0.9.0", "electron-devtools-installer": "^2.2.1", "electron-is-dev": "^0.3.0", diff --git a/src/yarn.lock b/src/yarn.lock index 9c6db3bc..a6362ec0 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -141,10 +141,6 @@ decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" -deepmerge@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" - dom-helpers@^3.2.0, dom-helpers@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" diff --git a/test/modules/environment.js b/test/modules/environment.js index 7ffab88f..e91ade43 100644 --- a/test/modules/environment.js +++ b/test/modules/environment.js @@ -39,6 +39,12 @@ module.exports = { }); }, + createTestUserDataDir() { + if (!fs.existsSync(userDataDir)) { + fs.mkdirSync(userDataDir); + } + }, + getSpectronApp() { return new Application({ path: electronBinaryPath, diff --git a/test/specs/app_test.js b/test/specs/app_test.js index 5e1f237e..6602ae59 100644 --- a/test/specs/app_test.js +++ b/test/specs/app_test.js @@ -8,17 +8,15 @@ describe('application', function desc() { this.timeout(30000); beforeEach(() => { + env.createTestUserDataDir(); env.cleanTestConfig(); this.app = env.getSpectronApp(); }); afterEach(() => { if (this.app && this.app.isRunning()) { - return this.app.stop().then(() => { - env.cleanTestConfig(); - }); + return this.app.stop(); } - env.cleanTestConfig(); return true; }); diff --git a/test/specs/settings_test.js b/test/specs/settings_test.js index 071fcc99..0b2b6fee 100644 --- a/test/specs/settings_test.js +++ b/test/specs/settings_test.js @@ -1,15 +1,9 @@ const settings = require('../../src/common/settings'); -const deepmerge = require('deepmerge'); +const buildConfig = require('../../src/common/config/buildConfig'); +const defaultPreferences = require('../../src/common/config/defaultPreferences'); +const pastDefaultPreferences = require('../../src/common/config/pastDefaultPreferences'); describe('common/settings.js', () => { - before(() => { - process.env.TEST = 1; - }); - - after(() => { - delete process.env.TEST; - }); - it('should upgrade v0 config file', () => { const v0Config = { url: 'https://example.com/team' @@ -20,13 +14,34 @@ describe('common/settings.js', () => { config.version.should.equal(settings.version); }); - it('should loadDefault config for version 1', () => { - const baseConfig = require('../../src/common/config/base.json'); - const overrideConfig = require('../../src/common/config/override.json'); - const expectedDefaults = deepmerge( - baseConfig[1], overrideConfig[1] || {}, {clone: true, arrayMerge: settings.deepMergeArray} - ); - const defaultConfig = settings.loadDefault(); - defaultConfig.should.eql(expectedDefaults); + it('should merge teams with buildConfig.defaultTeams', () => { + const teams = [ + { + name: 'test', + url: 'https://example.com' + } + ]; + + const mergedTeams = settings.mergeDefaultTeams(teams); + mergedTeams.should.deep.equal([ + { + name: 'test', + url: 'https://example.com' + }, + ...buildConfig.defaultTeams + ]); + }); +}); + +describe('common/config/', () => { + it('pastDefaultPreferences should have each past version of defaultPreferences', () => { + for (let version = 0; version <= defaultPreferences.version; version++) { + pastDefaultPreferences[`${version}`].should.exist; // eslint-disable-line no-unused-expressions + } + }); + + it('defaultPreferences equal to one of pastDefaultPreferences', () => { + const pastPreferences = pastDefaultPreferences[`${defaultPreferences.version}`]; + pastPreferences.should.deep.equal(defaultPreferences); }); }); diff --git a/yarn.lock b/yarn.lock index cd6192b4..565ab1d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,10 +1352,6 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" -deepmerge@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" - deepmerge@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.3.2.tgz#1663691629d4dbfe364fa12a2a4f0aa86aa3a050"