]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9044 Easy access to my organizations
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 10 May 2017 16:13:28 +0000 (18:13 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 12 May 2017 09:58:12 +0000 (11:58 +0200)
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.js
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.js.snap
server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index fca0171d9ec100d362cdb531e06748d28024abf9..86ed0974c5c6e250846df2222d078f4689d313c5 100644 (file)
@@ -21,7 +21,7 @@ import React from 'react';
 import { connect } from 'react-redux';
 import GlobalNavBranding from './GlobalNavBranding';
 import GlobalNavMenu from './GlobalNavMenu';
-import GlobalNavUser from './GlobalNavUser';
+import GlobalNavUserContainer from './GlobalNavUserContainer';
 import Search from '../../search/Search';
 import ShortcutsHelpView from './ShortcutsHelpView';
 import { getCurrentUser, getAppState } from '../../../../store/rootReducer';
@@ -76,7 +76,7 @@ class GlobalNav extends React.PureComponent {
                 </svg>
               </a>
             </li>
-            <GlobalNavUser {...this.props} />
+            <GlobalNavUserContainer {...this.props} />
           </ul>
         </div>
       </nav>
index 6d60902e1c846b0645815759f2e13d1f2ed88ad7..ac0f288819b3d144bb12d72c81d2f38c7a808743 100644 (file)
  */
 // @flow
 import React from 'react';
-import { Link, withRouter } from 'react-router';
+import classNames from 'classnames';
+import { sortBy } from 'lodash';
+import { Link } from 'react-router';
 import Avatar from '../../../../components/ui/Avatar';
+import OrganizationLink from '../../../../components/ui/OrganizationLink';
 import { translate } from '../../../../helpers/l10n';
 
-class GlobalNavUser extends React.PureComponent {
-  props: {
-    currentUser: {
-      email?: string,
-      name: string
-    },
-    location: Object,
-    router: { push: string => void }
+type CurrentUser = {
+  email?: string,
+  isLoggedIn: boolean,
+  name: string
+};
+
+type Props = {
+  currentUser: CurrentUser,
+  fetchMyOrganizations: () => Promise<*>,
+  location: Object,
+  organizations: Array<{ key: string, name: string }>,
+  router: { push: string => void }
+};
+
+type State = {
+  open: boolean
+};
+
+export default class GlobalNavUser extends React.PureComponent {
+  node: HTMLElement;
+  props: Props;
+  state: State = { open: false };
+
+  componentWillUnmount() {
+    window.removeEventListener('click', this.handleClickOutside);
+  }
+
+  handleClickOutside = (event: { target: HTMLElement }) => {
+    if (!this.node || !this.node.contains(event.target)) {
+      this.closeDropdown();
+    }
   };
 
-  handleLogin = e => {
+  handleLogin = (e: Event) => {
     e.preventDefault();
     const shouldReturnToCurrentPage = window.location.pathname !== `${window.baseUrl}/about`;
     if (shouldReturnToCurrentPage) {
@@ -45,36 +71,76 @@ class GlobalNavUser extends React.PureComponent {
     }
   };
 
-  handleLogout = e => {
+  handleLogout = (e: Event) => {
     e.preventDefault();
+    this.closeDropdown();
     this.props.router.push('/sessions/logout');
   };
 
+  toggleDropdown = (evt: Event) => {
+    evt.preventDefault();
+    if (this.state.open) {
+      this.closeDropdown();
+    } else {
+      this.openDropdown();
+    }
+  };
+
+  openDropdown = () => {
+    this.props.fetchMyOrganizations().then(() => {
+      window.addEventListener('click', this.handleClickOutside, true);
+      this.setState({ open: true });
+    });
+  };
+
+  closeDropdown = () => {
+    window.removeEventListener('click', this.handleClickOutside);
+    this.setState({ open: false });
+  };
+
   renderAuthenticated() {
-    const { currentUser } = this.props;
+    const { currentUser, organizations } = this.props;
+    const hasOrganizations = organizations.length > 0;
     return (
-      <li className="dropdown js-user-authenticated">
-        <a className="dropdown-toggle navbar-avatar" data-toggle="dropdown" href="#">
+      <li
+        className={classNames('dropdown js-user-authenticated', { open: this.state.open })}
+        ref={node => (this.node = node)}>
+        <a className="dropdown-toggle navbar-avatar" href="#" onClick={this.toggleDropdown}>
           <Avatar email={currentUser.email} name={currentUser.name} size={24} />
         </a>
-        <ul className="dropdown-menu dropdown-menu-right">
-          <li className="dropdown-item">
-            <div className="text-ellipsis text-muted" title={currentUser.name}>
-              <strong>{currentUser.name}</strong>
-            </div>
-            {currentUser.email != null &&
-              <div className="little-spacer-top text-ellipsis text-muted" title={currentUser.email}>
-                {currentUser.email}
-              </div>}
-          </li>
-          <li className="divider" />
-          <li>
-            <Link to="/account">{translate('my_account.page')}</Link>
-          </li>
-          <li>
-            <a onClick={this.handleLogout} href="#">{translate('layout.logout')}</a>
-          </li>
-        </ul>
+        {this.state.open &&
+          <ul className="dropdown-menu dropdown-menu-right">
+            <li className="dropdown-item">
+              <div className="text-ellipsis text-muted" title={currentUser.name}>
+                <strong>{currentUser.name}</strong>
+              </div>
+              {currentUser.email != null &&
+                <div
+                  className="little-spacer-top text-ellipsis text-muted"
+                  title={currentUser.email}>
+                  {currentUser.email}
+                </div>}
+            </li>
+            <li className="divider" />
+            <li>
+              <Link to="/account" onClick={this.closeDropdown}>{translate('my_account.page')}</Link>
+            </li>
+            {hasOrganizations && <li role="separator" className="divider" />}
+            {hasOrganizations &&
+              <li className="dropdown-header spacer-left">{translate('my_organizations')}</li>}
+            {hasOrganizations &&
+              sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
+                <li key={organization.key}>
+                  <OrganizationLink organization={organization} onClick={this.closeDropdown}>
+                    <span className="spacer-left">{organization.name}</span>
+                  </OrganizationLink>
+                </li>
+              ))}
+            {hasOrganizations && <li role="separator" className="divider" />}
+            <li>
+              <a onClick={this.handleLogout} href="#">{translate('layout.logout')}</a>
+            </li>
+          </ul>}
       </li>
     );
   }
@@ -91,5 +157,3 @@ class GlobalNavUser extends React.PureComponent {
     return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous();
   }
 }
-
-export default withRouter(GlobalNavUser);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js
new file mode 100644 (file)
index 0000000..c84bdff
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import { withRouter } from 'react-router';
+import { connect } from 'react-redux';
+import GlobalNavUser from './GlobalNavUser';
+import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions';
+import { getMyOrganizations } from '../../../../store/rootReducer';
+
+const mapStateToProps = state => ({
+  organizations: getMyOrganizations(state)
+});
+
+const mapDispatchToProps = {
+  fetchMyOrganizations
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(withRouter(GlobalNavUser));
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js
new file mode 100644 (file)
index 0000000..0a9f7fb
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import GlobalNavUser from '../GlobalNavUser';
+
+const currentUser = { isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' };
+const organizations = [
+  { key: 'myorg', name: 'MyOrg' },
+  { key: 'foo', name: 'Foo' },
+  { key: 'bar', name: 'bar' }
+];
+
+it('should render the right interface for anonymous user', () => {
+  const currentUser = { isLoggedIn: false };
+  const wrapper = shallow(
+    <GlobalNavUser currentUser={currentUser} fetchMyOrganizations={() => {}} organizations={[]} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the right interface for logged in user', () => {
+  const wrapper = shallow(
+    <GlobalNavUser currentUser={currentUser} fetchMyOrganizations={() => {}} organizations={[]} />
+  );
+  wrapper.setState({ open: true });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the users organizations', () => {
+  const wrapper = shallow(
+    <GlobalNavUser
+      currentUser={currentUser}
+      fetchMyOrganizations={() => {}}
+      organizations={organizations}
+    />
+  );
+  wrapper.setState({ open: true });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should update the component correctly when the user changes to anonymous', () => {
+  const fetchMyOrganizations = jest.fn();
+  const wrapper = shallow(
+    <GlobalNavUser
+      currentUser={currentUser}
+      fetchMyOrganizations={fetchMyOrganizations}
+      organizations={[]}
+    />
+  );
+  wrapper.setState({ open: true });
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setProps({ currentUser: { isLoggedIn: false } });
+  expect(fetchMyOrganizations.mock.calls.length).toBe(0);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should lazyload the organizations when opening the dropdown', () => {
+  const fetchMyOrganizations = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <GlobalNavUser
+      currentUser={currentUser}
+      fetchMyOrganizations={fetchMyOrganizations}
+      organizations={organizations}
+    />
+  );
+  expect(fetchMyOrganizations.mock.calls.length).toBe(0);
+  wrapper.instance().openDropdown();
+  expect(fetchMyOrganizations.mock.calls.length).toBe(1);
+  wrapper.instance().openDropdown();
+  expect(fetchMyOrganizations.mock.calls.length).toBe(2);
+});
+
+it('should update the organizations when the user changes', () => {
+  const fetchMyOrganizations = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <GlobalNavUser
+      currentUser={currentUser}
+      fetchMyOrganizations={fetchMyOrganizations}
+      organizations={organizations}
+    />
+  );
+  wrapper.instance().openDropdown();
+  expect(fetchMyOrganizations.mock.calls.length).toBe(1);
+  wrapper.setProps({
+    currentUser: { isLoggedIn: true, name: 'test', email: 'test@sonarsource.com' }
+  });
+  wrapper.instance().openDropdown();
+  expect(fetchMyOrganizations.mock.calls.length).toBe(2);
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap
new file mode 100644 (file)
index 0000000..50f52bb
--- /dev/null
@@ -0,0 +1,270 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render the right interface for anonymous user 1`] = `
+<li>
+  <a
+    href="#"
+    onClick={[Function]}
+  >
+    layout.login
+  </a>
+</li>
+`;
+
+exports[`should render the right interface for logged in user 1`] = `
+<li
+  className="dropdown js-user-authenticated open"
+>
+  <a
+    className="dropdown-toggle navbar-avatar"
+    href="#"
+    onClick={[Function]}
+  >
+    <Connect(Avatar)
+      email="foo@bar.baz"
+      name="foo"
+      size={24}
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li
+      className="dropdown-item"
+    >
+      <div
+        className="text-ellipsis text-muted"
+        title="foo"
+      >
+        <strong>
+          foo
+        </strong>
+      </div>
+      <div
+        className="little-spacer-top text-ellipsis text-muted"
+        title="foo@bar.baz"
+      >
+        foo@bar.baz
+      </div>
+    </li>
+    <li
+      className="divider"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account"
+      >
+        my_account.page
+      </Link>
+    </li>
+    <li>
+      <a
+        href="#"
+        onClick={[Function]}
+      >
+        layout.logout
+      </a>
+    </li>
+  </ul>
+</li>
+`;
+
+exports[`should render the users organizations 1`] = `
+<li
+  className="dropdown js-user-authenticated open"
+>
+  <a
+    className="dropdown-toggle navbar-avatar"
+    href="#"
+    onClick={[Function]}
+  >
+    <Connect(Avatar)
+      email="foo@bar.baz"
+      name="foo"
+      size={24}
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li
+      className="dropdown-item"
+    >
+      <div
+        className="text-ellipsis text-muted"
+        title="foo"
+      >
+        <strong>
+          foo
+        </strong>
+      </div>
+      <div
+        className="little-spacer-top text-ellipsis text-muted"
+        title="foo@bar.baz"
+      >
+        foo@bar.baz
+      </div>
+    </li>
+    <li
+      className="divider"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account"
+      >
+        my_account.page
+      </Link>
+    </li>
+    <li
+      className="divider"
+      role="separator"
+    />
+    <li
+      className="dropdown-header spacer-left"
+    >
+      my_organizations
+    </li>
+    <li>
+      <OrganizationLink
+        onClick={[Function]}
+        organization={
+          Object {
+            "key": "bar",
+            "name": "bar",
+          }
+        }
+      >
+        <span
+          className="spacer-left"
+        >
+          bar
+        </span>
+      </OrganizationLink>
+    </li>
+    <li>
+      <OrganizationLink
+        onClick={[Function]}
+        organization={
+          Object {
+            "key": "foo",
+            "name": "Foo",
+          }
+        }
+      >
+        <span
+          className="spacer-left"
+        >
+          Foo
+        </span>
+      </OrganizationLink>
+    </li>
+    <li>
+      <OrganizationLink
+        onClick={[Function]}
+        organization={
+          Object {
+            "key": "myorg",
+            "name": "MyOrg",
+          }
+        }
+      >
+        <span
+          className="spacer-left"
+        >
+          MyOrg
+        </span>
+      </OrganizationLink>
+    </li>
+    <li
+      className="divider"
+      role="separator"
+    />
+    <li>
+      <a
+        href="#"
+        onClick={[Function]}
+      >
+        layout.logout
+      </a>
+    </li>
+  </ul>
+</li>
+`;
+
+exports[`should update the component correctly when the user changes to anonymous 1`] = `
+<li
+  className="dropdown js-user-authenticated open"
+>
+  <a
+    className="dropdown-toggle navbar-avatar"
+    href="#"
+    onClick={[Function]}
+  >
+    <Connect(Avatar)
+      email="foo@bar.baz"
+      name="foo"
+      size={24}
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li
+      className="dropdown-item"
+    >
+      <div
+        className="text-ellipsis text-muted"
+        title="foo"
+      >
+        <strong>
+          foo
+        </strong>
+      </div>
+      <div
+        className="little-spacer-top text-ellipsis text-muted"
+        title="foo@bar.baz"
+      >
+        foo@bar.baz
+      </div>
+    </li>
+    <li
+      className="divider"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account"
+      >
+        my_account.page
+      </Link>
+    </li>
+    <li>
+      <a
+        href="#"
+        onClick={[Function]}
+      >
+        layout.logout
+      </a>
+    </li>
+  </ul>
+</li>
+`;
+
+exports[`should update the component correctly when the user changes to anonymous 2`] = `
+<li>
+  <a
+    href="#"
+    onClick={[Function]}
+  >
+    layout.login
+  </a>
+</li>
+`;
index 739948592e306d9d0b1cbfa587c11118196296ef..fe3ad60da2e8a7e9f8f5a3529ede3c60ff64b880 100644 (file)
@@ -31,34 +31,45 @@ type OwnProps = {
   params: { organizationKey: string }
 };
 
+type Props = {
+  children?: React.Element<*>,
+  location: Object,
+  organization: null | Organization,
+  params: { organizationKey: string },
+  fetchOrganization: string => Promise<*>
+};
+
 class OrganizationPage extends React.PureComponent {
   mounted: boolean;
-
-  props: {
-    children?: React.Element<*>,
-    location: Object,
-    organization: null | Organization,
-    params: { organizationKey: string },
-    fetchOrganization: string => Promise<*>
-  };
-
-  state = {
-    loading: true
-  };
+  props: Props;
+  state = { loading: true };
 
   componentDidMount() {
     this.mounted = true;
-    this.props.fetchOrganization(this.props.params.organizationKey).then(() => {
-      if (this.mounted) {
-        this.setState({ loading: false });
-      }
-    });
+    this.updateOrganization(this.props.params.organizationKey);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (nextProps.params.organizationKey !== this.props.params.organizationKey) {
+      this.updateOrganization(nextProps.params.organizationKey);
+    }
   }
 
   componentWillUnmount() {
     this.mounted = false;
   }
 
+  updateOrganization = (organizationKey: string) => {
+    if (this.mounted) {
+      this.setState({ loading: true });
+    }
+    this.props.fetchOrganization(organizationKey).then(() => {
+      if (this.mounted) {
+        this.setState({ loading: false });
+      }
+    });
+  };
+
   render() {
     const { organization } = this.props;
 
index 69afb1dc902e223535319e428f8bf549a40053c3..4ced6f1a369ec1c1695006c0463cc7a5d530d827 100644 (file)
@@ -23,7 +23,7 @@ import { UnconnectedOrganizationPage } from '../OrganizationPage';
 
 it('smoke test', () => {
   const wrapper = shallow(
-    <UnconnectedOrganizationPage>
+    <UnconnectedOrganizationPage params={{ organizationKey: 'foo' }}>
       <div>hello</div>
     </UnconnectedOrganizationPage>
   );
@@ -36,10 +36,23 @@ it('smoke test', () => {
 
 it('not found', () => {
   const wrapper = shallow(
-    <UnconnectedOrganizationPage>
+    <UnconnectedOrganizationPage params={{ organizationKey: 'foo' }}>
       <div>hello</div>
     </UnconnectedOrganizationPage>
   );
   wrapper.setState({ loading: false });
   expect(wrapper).toMatchSnapshot();
 });
+
+it('should correctly update when the organization changes', () => {
+  const fetchOrganization = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <UnconnectedOrganizationPage
+      params={{ organizationKey: 'foo' }}
+      fetchOrganization={fetchOrganization}>
+      <div>hello</div>
+    </UnconnectedOrganizationPage>
+  );
+  wrapper.setProps({ params: { organizationKey: 'bar' } });
+  expect(fetchOrganization.mock.calls).toMatchSnapshot();
+});
index 6c8f73738d410d63e89989aee3754376eabf16f7..d9579e1bf4a3dbbed550893b1b83a4f699c7fd68 100644 (file)
@@ -2,6 +2,14 @@
 
 exports[`not found 1`] = `<NotFound />`;
 
+exports[`should correctly update when the organization changes 1`] = `
+Array [
+  Array [
+    "bar",
+  ],
+]
+`;
+
 exports[`smoke test 1`] = `null`;
 
 exports[`smoke test 2`] = `
index ebe90b8f94751a763bc8151f8db46683257679a0..e17e86689634f28a6f189fe7f19c49dbd440520e 100644 (file)
@@ -28,7 +28,14 @@ it('should render list of projects with no selection', () => {
     { id: '2', key: 'b', name: 'B', qualifier: 'TRK' }
   ];
 
-  const result = shallow(<Projects projects={projects} selection={[]} refresh={jest.fn()} />);
+  const result = shallow(
+    <Projects
+      organization={{ key: 'foo' }}
+      projects={projects}
+      selection={[]}
+      refresh={jest.fn()}
+    />
+  );
   expect(result.find('tr').length).toBe(2);
   expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(0);
 });
@@ -41,7 +48,12 @@ it('should render list of projects with one selected', () => {
   const selection = ['a'];
 
   const result = shallow(
-    <Projects projects={projects} selection={selection} refresh={jest.fn()} />
+    <Projects
+      organization={{ key: 'foo' }}
+      projects={projects}
+      selection={selection}
+      refresh={jest.fn()}
+    />
   );
   expect(result.find('tr').length).toBe(2);
   expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(1);
index d09c631e58cb66d4f2ab92a4079b26b90b0c7ee2..6dbc794bc2437587d122bf131d9d5064a23e7ffb 100644 (file)
@@ -254,6 +254,7 @@ logging_out=You're logging out, please wait...
 manage=Manage
 move_left=Move left
 move_right=Move right
+my_organizations=My Organizations
 new_issues=New issues
 new_violations=New violations
 new_window=New window