diff --git a/README.md b/README.md
index f35dd32dc..99859696f 100644
--- a/README.md
+++ b/README.md
@@ -31,10 +31,10 @@ Run `npm install` to install the required node_modules for this project.
Run `ng serve` to run the app in dev mode. After executing this command, you can navigate to `http://localhost:4200/` to see the app working.
The app will automatically reload if you change anything in the source files.
-## Prepare your environment
+# Prepare your environment
### Set environment variables
-Create a file keys.ts in the path `src/enviroment` with the content pinned in our slack channel:
+**1**. Create a file keys.ts in the path `src/enviroment` with the content pinned in our slack channel #time-tracker-developer:
```
export const AUTHORITY = 'XXX';
@@ -44,7 +44,14 @@ export const STACK_EXCHANGE_ID = 'XXX';
export const STACK_EXCHANGE_ACCESS_TOKEN = 'XXX';
export const AZURE_APP_CONFIGURATION_CONNECTION_STRING = 'XXX';
```
-
+**2**. Create a second file `.keys.json` with the content pinned in the slack channel #time-tracker-developer:
+```
+{
+ "authority": 'XXX',
+ "client_id": 'XXX',
+ "scopes": ["XXX"]
+}
+```
### Prepare your environment for vscode
Install the following extensions:
@@ -82,6 +89,12 @@ Install the following extensions:
| `feat(pencil): add 'graphiteWidth' option` | ~~Minor~~ Feature Release |
| `perf(pencil): remove graphiteWidth option`
`BREAKING CHANGE: The graphiteWidth option has been removed.`
`The default graphite width of 10mm is always used for performance reasons.` | ~~Major~~ Breaking Release |
+### Branch names format
+For example if your task in Jira is **TT-48 implement semantic versioning** your branch name is:
+```
+ TT-48-implement-semantic-versioning
+```
+
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
@@ -127,4 +140,7 @@ To get more help on the Angular CLI use `ng help` or go check out the [Angular C
## Feature Toggles dictionary
Shared file with all the Feature Toggles we create, so we can have a history of them
-[Feature Toggles dictionary](https://github.com/ioet/time-tracker-ui/wiki/Feature-Toggles-dictionary)
\ No newline at end of file
+[Feature Toggles dictionary](https://github.com/ioet/time-tracker-ui/wiki/Feature-Toggles-dictionary)
+
+## More information about the project
+[Starting in Time Tracker](https://github.com/ioet/time-tracker-ui/wiki/Time-tracker)
\ No newline at end of file
diff --git a/src/app/modules/users/components/users-list/users-list.component.html b/src/app/modules/users/components/users-list/users-list.component.html
index bab11ae27..498d5e0dd 100644
--- a/src/app/modules/users/components/users-list/users-list.component.html
+++ b/src/app/modules/users/components/users-list/users-list.component.html
@@ -10,7 +10,7 @@
| User Email |
Names |
- Roles |
+ {{ isUserGroupsToggleOn ? 'Groups' : 'Roles' }} |
@@ -19,7 +19,22 @@
{{ user.email }} |
{{ user.name }} |
-
+
+
+ admin
+
+ test
+
+
+
{
let component: UsersListComponent;
let fixture: ComponentFixture;
let store: MockStore;
const actionSub: ActionsSubject = new ActionsSubject();
+ const fakeAppConfigurationConnectionString = 'Endpoint=http://fake.foo;Id=fake.id;Secret=fake.secret';
+ let service: FeatureManagerService;
+ let fakeFeatureToggleProvider;
const state: UserState = {
data: [
@@ -19,6 +36,7 @@ describe('UsersListComponent', () => {
name: 'name',
email: 'email',
roles: ['admin', 'test'],
+ groups: ['time-tracker-admin', 'time-tracker-tester'],
id: 'id',
tenant_id: 'tenant id',
deleted: 'delete',
@@ -30,10 +48,20 @@ describe('UsersListComponent', () => {
beforeEach(
waitForAsync(() => {
+ fakeFeatureToggleProvider = new FeatureToggleProvider(
+ new AppConfigurationClient(fakeAppConfigurationConnectionString),
+ new FeatureFilterProvider(new AzureAdB2CService())
+ );
+ service = new FeatureManagerService(fakeFeatureToggleProvider);
+
TestBed.configureTestingModule({
imports: [NgxPaginationModule, DataTablesModule],
declarations: [UsersListComponent],
- providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
+ providers: [
+ provideMockStore({ initialState: state }),
+ { provide: ActionsSubject, useValue: actionSub },
+ { provide: FeatureManagerService, useValue: service }
+ ],
}).compileComponents();
})
);
@@ -91,6 +119,27 @@ describe('UsersListComponent', () => {
});
});
+ const actionGroupParams = [
+ { actionType: UserActionTypes.ADD_USER_TO_GROUP_SUCCESS },
+ { actionType: UserActionTypes.REMOVE_USER_FROM_GROUP_SUCCESS },
+ ];
+
+ actionGroupParams.map((param) => {
+ it(`When action ${param.actionType} is dispatched should triggered load Users action`, () => {
+ spyOn(store, 'dispatch');
+
+ const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
+ const action = {
+ type: param.actionType,
+ payload: state.data,
+ };
+
+ actionSubject.next(action);
+
+ expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers());
+ });
+ });
+
const grantRoleTypes = [
{ roleId: 'admin', roleValue: 'time-tracker-admin' },
{ roleId: 'test', roleValue: 'time-tracker-tester' },
@@ -111,6 +160,32 @@ describe('UsersListComponent', () => {
});
});
+ const AddGroupTypes = [
+ { groupName: 'time-tracker-admin' },
+ { groupName: 'time-tracker-tester' }
+ ];
+
+ AddGroupTypes.map((param) => {
+ it(`When user switchGroup to ${param.groupName} and doesn't belong to any group, should add ${param.groupName} group to user`, () => {
+ const groupName = param.groupName;
+ const user = {
+ name: 'name',
+ email: 'email',
+ roles: [],
+ groups: [],
+ id: 'id',
+ tenant_id: 'tenant id',
+ deleted: 'delete',
+ } ;
+
+ spyOn(store, 'dispatch');
+
+ component.switchGroup(groupName, user);
+
+ expect(store.dispatch).toHaveBeenCalledWith(new AddUserToGroup(user.id, groupName));
+ });
+ });
+
const revokeRoleTypes = [
{ roleId: 'admin', roleValue: 'time-tracker-admin', userRoles: ['time-tracker-admin'] },
{ roleId: 'test', roleValue: 'time-tracker-tester', userRoles: ['time-tracker-tester'] },
@@ -131,6 +206,33 @@ describe('UsersListComponent', () => {
});
});
+ const removeGroupTypes = [
+ { groupName: 'time-tracker-admin', userGroups: ['time-tracker-admin'] },
+ { groupName: 'time-tracker-tester', userGroups: ['time-tracker-tester'] },
+ ];
+
+ removeGroupTypes.map((param) => {
+ it(`When user switchGroup to ${param.groupName} and belongs to group, should remove ${param.groupName} group from user`, () => {
+ const groupName = param.groupName;
+ const user = {
+ name: 'name',
+ email: 'email',
+ roles: [],
+ groups: param.userGroups,
+ id: 'id',
+ tenant_id: 'tenant id',
+ deleted: 'delete',
+ } ;
+
+
+ spyOn(store, 'dispatch');
+
+ component.switchGroup(groupName, user);
+
+ expect(store.dispatch).toHaveBeenCalledWith(new RemoveUserFromGroup(user.id, groupName));
+ });
+ });
+
it('on success load users, the data of roles should be an array', () => {
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
const action = {
@@ -145,6 +247,20 @@ describe('UsersListComponent', () => {
});
});
+ it('on success load users, the data of groups should be an array', () => {
+ const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
+ const action = {
+ type: UserActionTypes.LOAD_USERS_SUCCESS,
+ payload: state.data,
+ };
+
+ actionSubject.next(action);
+
+ component.users.map((user) => {
+ expect(user.groups).toEqual(['time-tracker-admin', 'time-tracker-tester']);
+ });
+ });
+
it('on success load users, the datatable should be reloaded', async () => {
const actionSubject = TestBed.inject(ActionsSubject);
const action = {
@@ -158,6 +274,27 @@ describe('UsersListComponent', () => {
expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
});
+ it('When Component is created, should call the feature toggle method', () => {
+ spyOn(component, 'isFeatureToggleActivated').and.returnValue(of(true));
+
+ component.ngOnInit();
+
+ expect(component.isFeatureToggleActivated).toHaveBeenCalled();
+ expect(component.isUserGroupsToggleOn).toBe(true);
+ });
+
+ const toggleValues = [true, false];
+ toggleValues.map((toggleValue) => {
+ it(`when FeatureToggle is ${toggleValue} should return ${toggleValue}`, () => {
+ spyOn(service, 'isToggleEnabledForUser').and.returnValue(of(toggleValue));
+
+ const isFeatureToggleActivated: Observable = component.isFeatureToggleActivated();
+
+ expect(service.isToggleEnabledForUser).toHaveBeenCalled();
+ isFeatureToggleActivated.subscribe((value) => expect(value).toEqual(toggleValue));
+ });
+ });
+
afterEach(() => {
component.dtTrigger.unsubscribe();
component.loadUsersSubscription.unsubscribe();
diff --git a/src/app/modules/users/components/users-list/users-list.component.ts b/src/app/modules/users/components/users-list/users-list.component.ts
index 8ac50f8b4..33ef09035 100644
--- a/src/app/modules/users/components/users-list/users-list.component.ts
+++ b/src/app/modules/users/components/users-list/users-list.component.ts
@@ -1,10 +1,18 @@
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
-import { ActionsSubject, select, Store } from '@ngrx/store';
+import { ActionsSubject, select, Store, Action } from '@ngrx/store';
import { DataTableDirective } from 'angular-datatables';
import { Observable, Subject, Subscription } from 'rxjs';
-import { delay, filter } from 'rxjs/operators';
+import { delay, filter, map } from 'rxjs/operators';
+import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service';
import { User } from '../../models/users';
-import { GrantRoleUser, LoadUsers, RevokeRoleUser, UserActionTypes } from '../../store/user.actions';
+import {
+ GrantRoleUser,
+ LoadUsers,
+ RevokeRoleUser,
+ UserActionTypes,
+ AddUserToGroup,
+ RemoveUserFromGroup,
+} from '../../store/user.actions';
import { getIsLoading } from '../../store/user.selectors';
@Component({
@@ -21,8 +29,15 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild(DataTableDirective, { static: false })
dtElement: DataTableDirective;
dtOptions: any = {};
+ switchGroupsSubscription: Subscription;
+ isEnableToggleSubscription: Subscription;
+ isUserGroupsToggleOn: boolean;
- constructor(private store: Store, private actionsSubject$: ActionsSubject) {
+ constructor(
+ private store: Store,
+ private actionsSubject$: ActionsSubject,
+ private featureManagerService: FeatureManagerService
+ ) {
this.isLoading$ = store.pipe(delay(0), select(getIsLoading));
}
@@ -35,6 +50,14 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
this.rerenderDataTable();
});
+ this.isEnableToggleSubscription = this.isFeatureToggleActivated().subscribe((flag) => {
+ this.isUserGroupsToggleOn = flag;
+ });
+
+ this.switchGroupsSubscription = this.filterUserGroup().subscribe((action) => {
+ this.store.dispatch(new LoadUsers());
+ });
+
this.switchRoleSubscription = this.actionsSubject$
.pipe(
filter(
@@ -55,6 +78,7 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnDestroy() {
this.loadUsersSubscription.unsubscribe();
this.dtTrigger.unsubscribe();
+ this.isEnableToggleSubscription.unsubscribe();
}
private rerenderDataTable(): void {
@@ -73,4 +97,27 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
? this.store.dispatch(new RevokeRoleUser(userId, roleId))
: this.store.dispatch(new GrantRoleUser(userId, roleId));
}
+
+ switchGroup(groupName: string, user: User): void {
+ this.store.dispatch(
+ user.groups.includes(groupName)
+ ? new RemoveUserFromGroup(user.id, groupName)
+ : new AddUserToGroup(user.id, groupName)
+ );
+ }
+
+ isFeatureToggleActivated(): Observable {
+ return this.featureManagerService.isToggleEnabledForUser('switch-group')
+ .pipe(map((enabled: boolean) => enabled));
+ }
+
+ filterUserGroup(): Observable {
+ return this.actionsSubject$.pipe(
+ filter(
+ (action: Action) =>
+ action.type === UserActionTypes.ADD_USER_TO_GROUP_SUCCESS ||
+ action.type === UserActionTypes.REMOVE_USER_FROM_GROUP_SUCCESS
+ )
+ );
+ }
}
|