Enable overriding connect defaults with query params, closes #49

This commit is contained in:
Ken-Håvard Lieng 2020-05-08 10:12:21 +02:00
parent dcbf3397c1
commit eab788a782
10 changed files with 177 additions and 328 deletions

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,26 @@ const getSortedDefaultChannels = createSelector(
channels => channels.split(',').sort() channels => channels.split(',').sort()
); );
const transformChannels = channels => {
const comma = channels[channels.length - 1] === ',';
channels = channels
.split(',')
.map(channel => {
channel = channel.trim();
if (channel) {
if (isValidChannel(channel, false) && channel[0] !== '#') {
channel = `#${channel}`;
}
}
return channel;
})
.filter(s => s)
.join(',');
return comma ? `${channels},` : channels;
};
class Connect extends Component { class Connect extends Component {
state = { state = {
showOptionals: false showOptionals: false
@ -51,26 +71,6 @@ class Connect extends Component {
return port; return port;
}; };
transformChannels = channels => {
const comma = channels[channels.length - 1] === ',';
channels = channels
.split(',')
.map(channel => {
channel = channel.trim();
if (channel) {
if (isValidChannel(channel, false) && channel[0] !== '#') {
channel = `#${channel}`;
}
}
return channel;
})
.filter(s => s)
.join(',');
return comma ? `${channels},` : channels;
};
render() { render() {
const { defaults, values } = this.props; const { defaults, values } = this.props;
const { readOnly, showDetails } = defaults; const { readOnly, showDetails } = defaults;
@ -117,7 +117,7 @@ class Connect extends Component {
<Error name="host" /> <Error name="host" />
<Error name="port" /> <Error name="port" />
<TextInput name="nick" /> <TextInput name="nick" />
<TextInput name="channels" transform={this.transformChannels} /> <TextInput name="channels" transform={transformChannels} />
{this.state.showOptionals && this.renderOptionals()} {this.state.showOptionals && this.renderOptionals()}
<Button <Button
className="connect-form-button-optionals" className="connect-form-button-optionals"
@ -140,24 +140,38 @@ class Connect extends Component {
export default withFormik({ export default withFormik({
enableReinitialize: true, enableReinitialize: true,
mapPropsToValues: ({ defaults }) => { mapPropsToValues: ({ defaults, query }) => {
let port = 6667; let port = '6667';
if (defaults.port) { if (query.port || defaults.port) {
({ port } = defaults); port = query.port || defaults.port;
} else if (defaults.ssl) { } else if (defaults.ssl) {
port = 6697; port = '6697';
}
let { channels } = query;
if (channels) {
channels = transformChannels(channels);
}
let ssl;
if (query.ssl === 'true') {
ssl = true;
} else if (query.ssl === 'false') {
ssl = false;
} else {
ssl = defaults.ssl || false;
} }
return { return {
name: defaults.name, name: query.name || defaults.name,
host: defaults.host, host: query.host || defaults.host,
port, port,
nick: '', nick: query.nick || '',
channels: defaults.channels.join(','), channels: channels || defaults.channels.join(','),
username: '', username: query.username || '',
password: defaults.password ? ' ' : '', password: defaults.password ? ' ' : '',
realname: '', realname: query.realname || '',
tls: defaults.ssl || false tls: ssl
}; };
}, },
validate: values => { validate: values => {
@ -203,6 +217,8 @@ export default withFormik({
const channels = values.channels ? values.channels.split(',') : []; const channels = values.channels ? values.channels.split(',') : [];
delete values.channels; delete values.channels;
values.password = values.password.trim();
values.port = `${values.port}`; values.port = `${values.port}`;
connect(values); connect(values);
select(values.host); select(values.host);

View File

@ -8,7 +8,8 @@ import connect from 'utils/connect';
const mapState = createStructuredSelector({ const mapState = createStructuredSelector({
defaults: getConnectDefaults, defaults: getConnectDefaults,
hexIP: state => getApp(state).hexIP hexIP: state => getApp(state).hexIP,
query: state => state.router.query
}); });
const mapDispatch = { const mapDispatch = {

View File

@ -85,7 +85,7 @@ describe('tab reducer', () => {
it('clears the tab when navigating to a non-tab page', () => { it('clears the tab when navigating to a non-tab page', () => {
let state = reducer(undefined, setSelectedTab('srv', '#chan')); let state = reducer(undefined, setSelectedTab('srv', '#chan'));
state = reducer(state, locationChanged('settings')); state = reducer(state, locationChanged('settings', {}, {}));
expect(state).toEqual({ expect(state).toEqual({
selected: {}, selected: {},
@ -96,10 +96,14 @@ describe('tab reducer', () => {
it('selects the tab and adds it to history when navigating to a tab', () => { it('selects the tab and adds it to history when navigating to a tab', () => {
const state = reducer( const state = reducer(
undefined, undefined,
locationChanged('chat', { locationChanged(
server: 'srv', 'chat',
name: '#chan' {
}) server: 'srv',
name: '#chan'
},
{}
)
); );
expect(state).toEqual({ expect(state).toEqual({

View File

@ -15,7 +15,8 @@ const initialState = {
windowWidth: 0, windowWidth: 0,
connectDefaults: { connectDefaults: {
name: '', name: '',
address: '', host: '',
port: '',
channels: [], channels: [],
ssl: false, ssl: false,
password: false, password: false,

View File

@ -6,11 +6,21 @@ export const PUSH = 'ROUTER_PUSH';
export const REPLACE = 'ROUTER_REPLACE'; export const REPLACE = 'ROUTER_REPLACE';
export function locationChanged(route, params, location) { export function locationChanged(route, params, location) {
Object.keys(params).forEach(key => {
params[key] = decodeURIComponent(params[key]);
});
const query = {};
new URLSearchParams(location.search).forEach((value, key) => {
query[key] = value;
});
return { return {
type: LOCATION_CHANGED, type: LOCATION_CHANGED,
route, route,
params, params,
location query,
path: decodeURIComponent(location.pathname)
}; };
} }
@ -28,13 +38,9 @@ export function replace(path) {
}; };
} }
export function routeReducer(state = {}, action) { export function routeReducer(state = {}, { type, ...action }) {
if (action.type === LOCATION_CHANGED) { if (type === LOCATION_CHANGED) {
return { return action;
route: action.route,
params: action.params,
location: action.location
};
} }
return state; return state;
@ -44,7 +50,7 @@ export function routeMiddleware() {
return next => action => { return next => action => {
switch (action.type) { switch (action.type) {
case PUSH: case PUSH:
history.push(action.path); history.push(`${action.path}`);
break; break;
case REPLACE: case REPLACE:
history.replace(action.path); history.replace(action.path);
@ -55,24 +61,13 @@ export function routeMiddleware() {
}; };
} }
function decode(location) {
location.pathname = decodeURIComponent(location.pathname);
return location;
}
function match(routes, location) { function match(routes, location) {
let params;
for (let i = 0; i < routes.length; i++) { for (let i = 0; i < routes.length; i++) {
params = routes[i].pattern.match(location.pathname); const params = routes[i].pattern.match(location.pathname);
if (params !== null) { if (params !== null) {
const keys = Object.keys(params); return locationChanged(routes[i].name, params, location);
for (let j = 0; j < keys.length; j++) {
params[keys[j]] = decodeURIComponent(params[keys[j]]);
}
return locationChanged(routes[i].name, params, decode({ ...location }));
} }
} }
return null;
} }
export default function initRouter(routes, store) { export default function initRouter(routes, store) {
@ -91,16 +86,11 @@ export default function initRouter(routes, store) {
let matched = match(patterns, history.location); let matched = match(patterns, history.location);
if (matched) { if (matched) {
store.dispatch(matched); store.dispatch(matched);
} else {
matched = { location: {} };
} }
history.listen(({ location }) => { history.listen(({ location }) => {
const nextMatch = match(patterns, location); const nextMatch = match(patterns, location);
if ( if (nextMatch && nextMatch.path !== matched?.path) {
nextMatch &&
nextMatch.location.pathname !== matched.location.pathname
) {
matched = nextMatch; matched = nextMatch;
store.dispatch(matched); store.dispatch(matched);
} }

View File

@ -26,6 +26,7 @@
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"brotli": "^1.3.1", "brotli": "^1.3.1",
"copy-webpack-plugin": "^5.1.1", "copy-webpack-plugin": "^5.1.1",
"cross-env": "^7.0.2",
"css-loader": "^3.5.3", "css-loader": "^3.5.3",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"del": "^5.1.0", "del": "^5.1.0",
@ -92,9 +93,9 @@
"test": "jest", "test": "jest",
"test:verbose": "jest --verbose", "test:verbose": "jest --verbose",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"gen:install": "GO111MODULE=off go get -u github.com/andyleap/gencode github.com/mailru/easyjson/...", "gen:install": "cross-env GO111MODULE=off go get -u github.com/andyleap/gencode github.com/mailru/easyjson/...",
"gen:binary": "gencode go -package storage -schema ../storage/storage.schema -unsafe", "gen:binary": "gencode go -package storage -schema ../storage/storage.schema -unsafe",
"gen:json": "easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go && easyjson -lower_camel_case -omit_empty ../storage/user.go" "gen:json": "cross-env GO111MODULE=off easyjson -all -lower_camel_case -omit_empty ../server/json.go ../server/index_data.go && cross-env GO111MODULE=off easyjson -lower_camel_case -omit_empty ../storage/user.go"
}, },
"jest": { "jest": {
"moduleNameMapper": { "moduleNameMapper": {

View File

@ -2544,6 +2544,12 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
cross-env@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9"
dependencies:
cross-spawn "^7.0.1"
cross-spawn@^6.0.0, cross-spawn@^6.0.5: cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5" version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -2554,7 +2560,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cross-spawn@^7.0.0: cross-spawn@^7.0.0, cross-spawn@^7.0.1:
version "7.0.2" version "7.0.2"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6"
dependencies: dependencies:

View File

@ -11,14 +11,8 @@ import (
) )
type connectDefaults struct { type connectDefaults struct {
Name string *config.Defaults
Host string Password bool
Port int
Channels []string
Password bool
SSL bool
ReadOnly bool
ShowDetails bool
} }
type dispatchVersion struct { type dispatchVersion struct {
@ -28,7 +22,7 @@ type dispatchVersion struct {
} }
type indexData struct { type indexData struct {
Defaults *config.Defaults Defaults connectDefaults
Servers []Server Servers []Server
Channels []*storage.Channel Channels []*storage.Channel
OpenDMs []storage.Tab OpenDMs []storage.Tab
@ -48,7 +42,7 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
cfg := d.Config() cfg := d.Config()
data := indexData{ data := indexData{
Defaults: &cfg.Defaults, Defaults: connectDefaults{Defaults: &cfg.Defaults},
HexIP: cfg.HexIP, HexIP: cfg.HexIP,
Version: dispatchVersion{ Version: dispatchVersion{
Tag: version.Tag, Tag: version.Tag,
@ -57,9 +51,7 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
}, },
} }
if data.Defaults.Password != "" { data.Defaults.Password = cfg.Defaults.Password != ""
data.Defaults.Password = "******"
}
if state == nil { if state == nil {
data.Settings = storage.DefaultClientSettings() data.Settings = storage.DefaultClientSettings()

View File

@ -39,14 +39,8 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer(in *jlexer.Lexer, out
} }
switch key { switch key {
case "defaults": case "defaults":
if in.IsNull() { if data := in.Raw(); in.Ok() {
in.Skip() in.AddError((out.Defaults).UnmarshalJSON(data))
out.Defaults = nil
} else {
if out.Defaults == nil {
out.Defaults = new(config.Defaults)
}
easyjson7e607aefDecodeGithubComKhliengDispatchConfig(in, out.Defaults)
} }
case "servers": case "servers":
if in.IsNull() { if in.IsNull() {
@ -183,11 +177,11 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer(out *jwriter.Writer, i
out.RawByte('{') out.RawByte('{')
first := true first := true
_ = first _ = first
if in.Defaults != nil { if true {
const prefix string = ",\"defaults\":" const prefix string = ",\"defaults\":"
first = false first = false
out.RawString(prefix[1:]) out.RawString(prefix[1:])
easyjson7e607aefEncodeGithubComKhliengDispatchConfig(out, *in.Defaults) out.Raw((in.Defaults).MarshalJSON())
} }
if len(in.Servers) != 0 { if len(in.Servers) != 0 {
const prefix string = ",\"servers\":" const prefix string = ",\"servers\":"
@ -448,163 +442,6 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer,
} }
out.RawByte('}') out.RawByte('}')
} }
func easyjson7e607aefDecodeGithubComKhliengDispatchConfig(in *jlexer.Lexer, out *config.Defaults) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "name":
out.Name = string(in.String())
case "host":
out.Host = string(in.String())
case "port":
out.Port = int(in.Int())
case "channels":
if in.IsNull() {
in.Skip()
out.Channels = nil
} else {
in.Delim('[')
if out.Channels == nil {
if !in.IsDelim(']') {
out.Channels = make([]string, 0, 4)
} else {
out.Channels = []string{}
}
} else {
out.Channels = (out.Channels)[:0]
}
for !in.IsDelim(']') {
var v10 string
v10 = string(in.String())
out.Channels = append(out.Channels, v10)
in.WantComma()
}
in.Delim(']')
}
case "password":
out.Password = string(in.String())
case "ssl":
out.SSL = bool(in.Bool())
case "readOnly":
out.ReadOnly = bool(in.Bool())
case "showDetails":
out.ShowDetails = bool(in.Bool())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson7e607aefEncodeGithubComKhliengDispatchConfig(out *jwriter.Writer, in config.Defaults) {
out.RawByte('{')
first := true
_ = first
if in.Name != "" {
const prefix string = ",\"name\":"
first = false
out.RawString(prefix[1:])
out.String(string(in.Name))
}
if in.Host != "" {
const prefix string = ",\"host\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Host))
}
if in.Port != 0 {
const prefix string = ",\"port\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Int(int(in.Port))
}
if len(in.Channels) != 0 {
const prefix string = ",\"channels\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
{
out.RawByte('[')
for v11, v12 := range in.Channels {
if v11 > 0 {
out.RawByte(',')
}
out.String(string(v12))
}
out.RawByte(']')
}
}
if in.Password != "" {
const prefix string = ",\"password\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Password))
}
if in.SSL {
const prefix string = ",\"ssl\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Bool(bool(in.SSL))
}
if in.ReadOnly {
const prefix string = ",\"readOnly\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Bool(bool(in.ReadOnly))
}
if in.ShowDetails {
const prefix string = ",\"showDetails\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Bool(bool(in.ShowDetails))
}
out.RawByte('}')
}
func easyjson7e607aefDecodeGithubComKhliengDispatchServer1(in *jlexer.Lexer, out *dispatchVersion) { func easyjson7e607aefDecodeGithubComKhliengDispatchServer1(in *jlexer.Lexer, out *dispatchVersion) {
isTopLevel := in.IsStart() isTopLevel := in.IsStart()
if in.IsNull() { if in.IsNull() {
@ -705,6 +542,7 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
in.Skip() in.Skip()
return return
} }
out.Defaults = new(config.Defaults)
in.Delim('{') in.Delim('{')
for !in.IsDelim('}') { for !in.IsDelim('}') {
key := in.UnsafeFieldName(false) key := in.UnsafeFieldName(false)
@ -715,6 +553,8 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
continue continue
} }
switch key { switch key {
case "password":
out.Password = bool(in.Bool())
case "name": case "name":
out.Name = string(in.String()) out.Name = string(in.String())
case "host": case "host":
@ -737,15 +577,13 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
out.Channels = (out.Channels)[:0] out.Channels = (out.Channels)[:0]
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v13 string var v10 string
v13 = string(in.String()) v10 = string(in.String())
out.Channels = append(out.Channels, v13) out.Channels = append(out.Channels, v10)
in.WantComma() in.WantComma()
} }
in.Delim(']') in.Delim(']')
} }
case "password":
out.Password = bool(in.Bool())
case "ssl": case "ssl":
out.SSL = bool(in.Bool()) out.SSL = bool(in.Bool())
case "readOnly": case "readOnly":
@ -766,10 +604,20 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer2(out *jwriter.Writer,
out.RawByte('{') out.RawByte('{')
first := true first := true
_ = first _ = first
if in.Name != "" { if in.Password {
const prefix string = ",\"name\":" const prefix string = ",\"password\":"
first = false first = false
out.RawString(prefix[1:]) out.RawString(prefix[1:])
out.Bool(bool(in.Password))
}
if in.Name != "" {
const prefix string = ",\"name\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Name)) out.String(string(in.Name))
} }
if in.Host != "" { if in.Host != "" {
@ -802,25 +650,15 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer2(out *jwriter.Writer,
} }
{ {
out.RawByte('[') out.RawByte('[')
for v14, v15 := range in.Channels { for v11, v12 := range in.Channels {
if v14 > 0 { if v11 > 0 {
out.RawByte(',') out.RawByte(',')
} }
out.String(string(v15)) out.String(string(v12))
} }
out.RawByte(']') out.RawByte(']')
} }
} }
if in.Password {
const prefix string = ",\"password\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Bool(bool(in.Password))
}
if in.SSL { if in.SSL {
const prefix string = ",\"ssl\":" const prefix string = ",\"ssl\":"
if first { if first {