diff --git a/.gitignore b/.gitignore index c9f23a4c..5d45cc12 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,10 @@ package-lock.json *.sql *.sqlite +# IDE generated files # +####################### +.idea + # OS generated files # ###################### .DS_Store diff --git a/package.json b/package.json index dd705d37..271294b4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "prepare": "patch-package", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", - "update-api": "yarn add github:bcgov/jag-shuber-api", + "update-api": "yarn remove jag-shuber-api && yarn add github:bcgov/jag-shuber-api#feature/implement-roles", "snyk-protect": "snyk protect", "prepublish": "npm run snyk-protect" }, @@ -24,6 +24,9 @@ "chalk": "1.1.3", "color": "^3.0.0", "css-loader": "0.28.4", + "deep-diff": "^1.0.2", + "deep-object-diff": "^1.1.0", + "deepmerge": "^4.2.2", "dotenv": "4.0.0", "extract-text-webpack-plugin": "3.0.0", "file-loader": "^1.1.11", @@ -33,11 +36,12 @@ "interact.js": "^1.2.8", "interactjs": "^1.3.4", "istanbul": "^0.4.5", - "jag-shuber-api": "github:bcgov/jag-shuber-api", + "jag-shuber-api": "github:bcgov/jag-shuber-api#feature/implement-roles", "jest": "20.0.4", "map": "^1.0.1", "moment": "^2.22.2", "object-assign": "4.1.1", + "object-diff": "^0.0.4", "open-iconic": "^1.1.1", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.6", @@ -45,7 +49,7 @@ "rc-slider": "^8.6.1", "rc-tooltip": "^3.7.2", "react": "^16.4.2", - "react-bootstrap": "^0.32.0", + "react-bootstrap": "^0.32.4", "react-calendar-timeline": "^0.22.0", "react-datetime": "^2.11.1", "react-dev-utils": "^4.2.1", @@ -63,6 +67,7 @@ "redux-modal": "^1.7.3", "redux-thunk": "^2.2.0", "reselect": "^3.0.1", + "snyk": "^1.120.0", "source-map-loader": "^0.2.3", "style-loader": "0.18.2", "superagent": "^3.8.3", @@ -73,8 +78,7 @@ "webpack-manifest-plugin": "1.2.1", "webpack-version-file": "^0.1.3", "whatwg-fetch": "2.0.3", - "yarn": "^1.9.4", - "snyk": "^1.120.0" + "yarn": "^1.9.4" }, "devDependencies": { "@storybook/addon-actions": "^3.3.10", @@ -93,7 +97,7 @@ "@types/prop-types": "^15.5.2", "@types/rc-slider": "^8.2.3", "@types/rc-tooltip": "^3.4.11", - "@types/react": "16.0.25", + "@types/react": "16.4.2", "@types/react-bootstrap": "^0.31.8", "@types/react-bootstrap-date-picker": "^4.0.4", "@types/react-dom": "^16.0.3", diff --git a/src/app/RootLayout.tsx b/src/app/RootLayout.tsx index 061a72e0..5a542018 100644 --- a/src/app/RootLayout.tsx +++ b/src/app/RootLayout.tsx @@ -9,36 +9,55 @@ import { DragDropContext } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; -import Navigation from './components/Navigation'; -import DutyRoster from './pages/DutyRoster'; -import ManageSheriffs from './pages/ManageSheriffs'; -import DefaultAssignments from './pages/DefaultAssignments'; -import Scheduling from './pages/Scheduling'; -import AssignmentDutyEditModal from './containers/AssignmentDutyEditModal'; -import LocationSelector from './containers/LocationSelector'; + import { Well, Alert, Button } from 'react-bootstrap'; -import SheriffProfileModal from './containers/SheriffProfileModal'; -import ScheduleShiftCopyModal from './containers/ScheduleShiftCopyModal'; -import ScheduleShiftAddModal from './containers/ScheduleShiftAddModal'; -import AssignmentSheriffDutyReassignmentModal from './containers/AssignmentSheriffDutyReassignmentModal'; -import PublishSchedule from './pages/PublishSchedule/PublishSchedule'; -import Footer from './components/Footer/Footer'; + +// Import generic infrastructure constructs and modules +import resolveAppUrl from './infrastructure/resolveAppUrl'; +import CustomDragLayer from './infrastructure/DragDrop/CustomDragLayer'; import { isLocationSet as isCurrentLocationSet, isLoggedIn as isUserLoggedIn, isLoadingToken as isLoadingUserToken, loadingTokenError } from './modules/user/selectors'; + +// Import core layout components +import Navigation from './components/Navigation'; + +// Import pages +import AuditPage from './pages/Audit'; +import AssignmentPage from './pages/Assignments'; +import DefaultAssignmentsPage from './pages/DefaultAssignments'; +import DutyRosterPage from './pages/DutyRoster'; +import ManageCourtroomsPage from './pages/ManageCourtrooms'; +import ManageCodesPage from './pages/ManageCodes'; +import ManageComponentsPage from './pages/ManageComponents'; +import ManageApisPage from './pages/ManageApis'; +import ManageRolesPage from './pages/ManageRoles'; +import ManageUserRolesPage from './pages/ManageUserRoles'; +import ManageSheriffsPage from './pages/ManageSheriffs'; +import ManageUsersPage from './pages/ManageUsers'; +import SchedulingPage from './pages/Scheduling'; +import PublishSchedulePage from './pages/PublishSchedule/PublishSchedule'; + +// Import container components import ToastManager from './components/ToastManager/ToastManager'; +import LocationSelector from './containers/NavLocationSelector'; +import Footer from './components/Footer/Footer'; + +// Import app-level modal components import ConnectedConfirmationModal from './containers/ConfirmationModal'; -import SheriffProfileCreateModal from './containers/SheriffProfileCreateModal'; -import resolveAppUrl from './infrastructure/resolveAppUrl'; -import CustomDragLayer from './infrastructure/DragDrop/CustomDragLayer'; -import ScheduleShiftMultiEditModal from './containers/ScheduleMultiShiftEditModal'; -import DutyRosterToolsModal from './containers/DutyRosterToolsModal'; -import AssignmentPage from './pages/Assignments'; import AssignmentScheduleAddModal from './containers/AssignmentScheduleAddModal'; import AssignmentScheduleEditModal from './containers/AssignmentScheduleEditModal'; +import AssignmentDutyEditModal from './containers/AssignmentDutyEditModal'; +import AssignmentSheriffDutyReassignmentModal from './containers/AssignmentSheriffDutyReassignmentModal'; +import DutyRosterToolsModal from './containers/DutyRosterToolsModal'; +import ScheduleShiftCopyModal from './containers/ScheduleShiftCopyModal'; +import ScheduleShiftAddModal from './containers/ScheduleShiftAddModal'; +import ScheduleShiftMultiEditModal from './containers/ScheduleMultiShiftEditModal'; +import SheriffProfileModal from './containers/SheriffProfileModal'; +import SheriffProfileCreateModal from './containers/SheriffProfileCreateModal'; export interface LayoutStateProps { isLocationSet?: boolean; @@ -49,6 +68,7 @@ export interface LayoutStateProps { export interface LayoutDispatchProps { } + class Layout extends React.Component { componentWillReceiveProps(nextProps: LayoutStateProps) { @@ -93,10 +113,11 @@ class Layout extends React.Component {
@@ -109,12 +130,20 @@ class Layout extends React.Component { {isLocationSet && (
- - - - + + + + - + + + + + + + + + @@ -125,7 +154,7 @@ class Layout extends React.Component { - +
)}
diff --git a/src/app/api/Api.ts b/src/app/api/Api.ts index 4623e815..10bfbc1c 100644 --- a/src/app/api/Api.ts +++ b/src/app/api/Api.ts @@ -26,6 +26,17 @@ export type LeaveSubCodeMap = MapType; export type LeaveCancelCodeMap = MapType; export type CourtRoleMap = MapType; export type GenderCodeMap = MapType; +export type RoleMap = MapType; +export type RolePermissionMap = MapType; +export type RoleFrontendScopeMap = MapType; +export type RoleApiScopeMap = MapType; +export type FrontendScopeMap = MapType; +export type FrontendScopePermissionMap = MapType; +export type RoleFrontendScopePermissionMap = MapType; +export type RoleApiScopePermissionMap = MapType; +export type ApiScopeMap = MapType; +export type UserMap = MapType; +export type UserRoleMap = MapType; export const WORK_SECTIONS: StringMap = { COURTS: 'Courts', @@ -108,7 +119,7 @@ export namespace Leave { export namespace WorkSection { export function getWorkSectionSortCode(workSectionId?: WorkSectionCode): string { - switch(workSectionId) { + switch (workSectionId) { case 'COURTS': return '0'; case 'JAIL': @@ -120,7 +131,7 @@ export namespace WorkSection { default: return '4'; } - } + } } export const BLANK_SHERIFF: Sheriff = { @@ -162,6 +173,7 @@ export interface SheriffProfile { sheriff: Sheriff; leaves?: Leave[]; } + export interface Sheriff { id: IdType; firstName: string; @@ -194,7 +206,7 @@ export interface BaseAssignment { locationId: IdType; workSectionId: WorkSectionCode; dutyRecurrences?: DutyRecurrence[]; - startDateTime: DateType; + startDateTime: DateType; endDateTime?: DateType; } @@ -250,6 +262,7 @@ export interface SheriffDutyReassignmentDetails { targetSheriffDuty: SheriffDuty; newTargetDutyStartTime: DateType; } + export interface DutyRecurrence { id?: IdType; assignmentId?: IdType; @@ -335,6 +348,7 @@ export interface Leave { } export interface LeaveSubCode { + id?: IdType; // Used on client-side only code: string; subCode: string; description: string; @@ -368,8 +382,214 @@ export interface AssignmentScheduleItem { workSectionId: WorkSectionCode; } -export interface API { +// Users, roles & permissions +export interface User { + id?: IdType; + displayName?: string; + defaultLocationId?: IdType; + systemAccountInd?: number; + sheriffId?: IdType; + sheriff?: Sheriff; + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} +export interface Role { + id: IdType; + roleName?: string; + roleCode?: string; + systemCodeInd?: number; + description?: string; + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface UserRole { + id?: IdType; + userId?: IdType; + roleId?: IdType; + effectiveDate?: string; + expiryDate?: string; + locationId?: IdType; + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface ApiScope { + id?: IdType; + apiScopeId?: string; + scopeName?: string; // Human-friendly scope name + scopeCode?: string; // Code type for the scope + systemScopeInd?: string; // Is the scope required by the SYSTEM + description?: string; // Scope description + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface FrontendScope { + id?: IdType; + frontendScopeId?: string; + scopeName?: string; // Human-friendly scope name + scopeCode?: string; // Code type for the scope + systemScopeInd?: string; // Is the scope required by the SYSTEM + description?: string; // Scope description + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface FrontendScopePermission { + id?: IdType; + frontendScopeId?: string; + permissionCode?: string; + displayName?: string; + description?: string; + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface ApiScopePermission { + id?: IdType; + apiScopeId?: string; + permissionCode?: string; + displayName?: string; + description?: string; + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface RoleApiScope { + id?: IdType; + roleId?: string; + scopeId?: string; + // TODO: I think we can rip rolePermissions out, we're not using it + rolePermissions: Array; + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface RoleFrontendScope { + id?: IdType; + roleId?: string; + scopeId?: string; + // TODO: I think we can rip rolePermissions out, we're not using it + rolePermissions: Array; + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +/** + * Scoped access to API routes. + */ +export interface RolePermission { + id?: IdType; + roleId?: string; + roleApiScopeId?: string, + roleFrontendScopeId?: string, + frontendScopePermissionId?: string, + displayName?: string; // TODO: This should be client-side only! + description?: string; // TODO: This should be client-side only! + createdBy?: string; + updatedBy?: string; + createdDtm?: string; + updatedDtm?: string; + revisionCount?: number; +} + +export interface RoleFrontendScopePermission extends RolePermission { + scope?: FrontendScope; + roleScope?: RoleFrontendScope; + scopePermission?: FrontendScopePermission; + // hasPermission is not in the API entity + // It is only used on the client-side + hasPermission?: boolean; +} +export interface RoleApiScopePermission extends RolePermission { + scope?: ApiScope; + roleScope?: RoleApiScope; + scopePermission?: ApiScopePermission; + // hasPermission is not in the API entity + // It is only used on the client-side + hasPermission?: boolean; +} + +// Queries +// First some base stuff +export interface ExpirableRecordQuery { + effectiveDate?: DateType; // TODO: Verify that these DateTypes will work + expiryDate?: DateType; // TODO: Verify that these DateTypes will work +} + +export interface RecordMetaQuery { + createdBy?: string; + createdDtm?: DateType; // TODO: Verify that these DateTypes will work + updatedBy?: string; + updatedDtm?: DateType; // TODO: Verify that these DateTypes will work + revisionCount?: number; // TODO: Verify that these DateTypes will work +} + +export interface CodeQuery { + code?: string; +} + +export interface ScopeQuery { + scopeName?: string; + scopeCode?: string; +} + +// Concrete query types +export interface RoleQuery extends RecordMetaQuery { + roleName?: string; + roleCode?: string; +} + +export interface SheriffQuery extends RecordMetaQuery { + firstName?: string; + lastName?: string; + badgeNo?: string | number; + sheriffRankCode?: string; + currentLocationId?: string; + homeLocationId?: string; + genderCode?: string; +} + +// TODO: Or should it be the other way around - SheriffQuery extends UserQuery? +export interface UserQuery extends SheriffQuery { + displayName?: string; + locationId?: string; + defaultLocationId?: string; +} + +export interface ApiScopeQuery extends ScopeQuery {} +export interface FrontendScopeQuery extends ScopeQuery {} + +export interface API { // Sheriffs getSheriffs(): Promise; createSheriff(newSheriff: Sheriff): Promise; @@ -412,9 +632,19 @@ export interface API { createLeave(newLeave: Partial): Promise; updateLeave(updatedLeave: Leave): Promise; getLeaveSubCodes(): Promise; + createLeaveSubCode(newLeaveSubCode: Partial): Promise; + updateLeaveSubCode(updatedLeaveSubCode: LeaveSubCode): Promise; + deleteLeaveSubCode(subCodeId: IdType): Promise; + deleteLeaveSubCodes(ids: IdType[]): Promise; getLeaveCancelCodes(): Promise; + // Courtrooms getCourtrooms(): Promise; + createCourtroom(newCourtroom: Partial): Promise; + updateCourtroom(updatedCourtroom: Partial): Promise; + deleteCourtroom(courtroomId: IdType): Promise; + deleteCourtrooms(courtroomIds: IdType[]): Promise; + getEscortRuns(): Promise; getJailRoles(): Promise; getAlternateAssignmentTypes(): Promise; @@ -424,6 +654,68 @@ export interface API { getLocations(): Promise; + // Users, roles & permissions + getUser(id: IdType): Promise; + createUser(newUser: Partial): Promise; + updateUser(updatedUser: User): Promise; + deleteUser(userId: IdType): Promise; + getUsers(): Promise; + deleteUsers(ids: IdType[]): Promise; + + getUserRole(): Promise; + createUserRole(newUserRole: Partial): Promise; + updateUserRole(updatedUserRole: UserRole): Promise; + deleteUserRole(id: IdType): Promise; + getUserRoles(): Promise; + deleteUserRoles(ids: IdType[]): Promise; + + getRole(): Promise; + createRole(newRole: Partial): Promise; + updateRole(updatedRole: Role): Promise; + deleteRole(roleId: IdType): Promise; + getRoles(): Promise; + deleteRoles(ids: IdType[]): Promise; + + getRolePermission(): Promise; + createRolePermission(newRolePermission: Partial): Promise; + updateRolePermission(updatedRolePermission: RolePermission): Promise; + getRolePermissions(): Promise; + deleteRolePermissions(permissionIds: IdType[]): Promise; + + getFrontendScope(): Promise; + createFrontendScope(newFrontendScope: Partial): Promise; + updateFrontendScope(updatedFrontendScope: FrontendScope): Promise; + getFrontendScopes(): Promise; + deleteFrontendScope(frontendScopeId: IdType): Promise; + deleteFrontendScopes(frontendScopeIds: IdType[]): Promise; + + getFrontendScopePermission(): Promise; + createFrontendScopePermission(newFrontendScopePermission: Partial): Promise; + updateFrontendScopePermission(updatedFrontendScopePermission: FrontendScopePermission): Promise; + getFrontendScopePermissions(): Promise; + deleteFrontendScopePermissions(frontendScopePermissionIds: IdType[]): Promise; + + getApiScope(): Promise; + createApiScope(newApiScope: Partial): Promise; + updateApiScope(updatedApiScope: ApiScope): Promise; + getApiScopes(): Promise; + deleteApiScope(apiScopeId: IdType): Promise; + deleteApiScopes(apiScopeIds: IdType[]): Promise; + + getRoleFrontendScope(): Promise; + createRoleFrontendScope(newRoleFrontendScope: Partial): Promise; + updateRoleFrontendScope(updatedRoleFrontendScope: RoleFrontendScope): Promise; + getRoleFrontendScopes(): Promise; + deleteRoleFrontendScope(roleFrontendScopeIds: IdType): Promise; + deleteRoleFrontendScopes(roleFrontendScopeIds: IdType[]): Promise; + + getRoleApiScope(): Promise; + createRoleApiScope(newRoleApiScope: Partial): Promise; + updateRoleApiScope(updatedRoleApiScope: RoleApiScope): Promise; + getRoleApiScopes(): Promise; + deleteRoleApiScope(roleApiScopeId: IdType): Promise; + deleteRoleApiScopes(roleApiScopeIds: IdType[]): Promise; + getToken(): Promise; logout(): Promise; -} \ No newline at end of file +} diff --git a/src/app/api/Client.ts b/src/app/api/Client.ts index 92a069b0..ecf77295 100644 --- a/src/app/api/Client.ts +++ b/src/app/api/Client.ts @@ -28,7 +28,16 @@ import { LeaveCancelCode, CourtRole, GenderCode, - SheriffDutyReassignmentDetails + SheriffDutyReassignmentDetails, + User, + Role, + RolePermission, + FrontendScope, + FrontendScopePermission, + ApiScope, + RoleFrontendScope, + RoleApiScope, + UserRole } from './Api'; import { SubmissionError } from 'redux-form'; @@ -107,7 +116,13 @@ export default class Client implements API { } async getSheriffs(): Promise { - const sheriffList = (await this._client.GetSheriffs(this.currentLocation) as Sheriff[]); + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + + const sheriffList = (await this._client.GetSheriffs(currentLocation) as Sheriff[]); return sheriffList; } @@ -134,13 +149,25 @@ export default class Client implements API { async getAssignments(dateRange: DateRange = {}): Promise<(CourtAssignment | JailAssignment | EscortAssignment | OtherAssignment)[]> { const { startDate, endDate } = dateRange; - const list = await this._client.GetAssignments(this.currentLocation, startDate, endDate); + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + + const list = await this._client.GetAssignments(currentLocation, startDate, endDate); return list as Assignment[]; } async createAssignment(assignment: Partial): Promise { + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + const assignmentToCreate: any = { ...assignment, - locationId: this.currentLocation + locationId: currentLocation }; const created = await this._client.CreateAssignment(assignmentToCreate); return created as Assignment; @@ -167,9 +194,11 @@ export default class Client implements API { await Promise.all(assignmentIds.map(id => this._client.ExpireAssignment(id))); } + // TODO: getAssignmentDuties broke before, not sure why!!! Is this still an issue? async getAssignmentDuties(startDate: DateType = moment(), endDate?: DateType): Promise { - let duties: AssignmentDuty[] = (await this._client.GetDuties() as any); - return duties; + // let duties: AssignmentDuty[] = (await this._client.GetDuties() as any); + // return duties; + return Promise.resolve([] as AssignmentDuty[]); } async createAssignmentDuty(duty: Partial): Promise { @@ -276,21 +305,39 @@ export default class Client implements API { } async createDefaultDuties(date: moment.Moment = moment()): Promise { + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + return await this._client.ImportDefaultDuties({ - locationId: this.currentLocation, + locationId: currentLocation, date: date.toISOString() }) as AssignmentDuty[]; } async autoAssignSheriffDuties(date: moment.Moment = moment()): Promise { + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + return await this._client.AutoAssignSheriffDuties({ - locationId: this.currentLocation, + locationId: currentLocation, date: date.toISOString() }) as SheriffDuty[]; } async getShifts(): Promise { - const list = await this._client.GetShifts(this.currentLocation); + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + + const list = await this._client.GetShifts(currentLocation); return list as Shift[]; } @@ -315,9 +362,15 @@ export default class Client implements API { } async createShift(newShift: Partial): Promise { + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + const shiftToCreate: any = { ...newShift, - locationId: this.currentLocation + locationId: currentLocation }; const created = await this._client.CreateShift(shiftToCreate); return created as Shift; @@ -329,11 +382,17 @@ export default class Client implements API { async copyShifts(shiftCopyDetails: ShiftCopyOptions): Promise { const { startOfWeekDestination, startOfWeekSource, shouldIncludeSheriffs } = shiftCopyDetails; + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + return await this._client.CopyShifts({ startOfWeekDestination: moment(startOfWeekDestination).toISOString(), startOfWeekSource: moment(startOfWeekSource).toISOString(), shouldIncludeSheriffs, - locationId: this.currentLocation + locationId: currentLocation }) as Shift[]; } @@ -363,6 +422,29 @@ export default class Client implements API { return this._client.GetLeaveSubCodes() as Promise; } + createLeaveSubCode(subCode: Partial): Promise { + return Promise.resolve({} as LeaveSubCode); + } + + updateLeaveSubCode(subCode: Partial): Promise { + const { code } = subCode; + if (!code) { + throw 'No code included in the sub code to update'; + } + return Promise.resolve({} as LeaveSubCode); + } + + deleteLeaveSubCode(code: IdType): Promise { + return Promise.resolve(); + } + + deleteLeaveSubCodes(ids: IdType[]): Promise { + if (ids.length > 0) { + } + + return Promise.resolve(); + } + getLeaveCancelCodes(): Promise { return this._client.GetLeaveCancelReasonCodes() as Promise; } @@ -373,12 +455,50 @@ export default class Client implements API { } async getCourtrooms(): Promise { - const list = await this._client.GetCourtrooms(this.currentLocation); + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + + const list = await this._client.GetCourtrooms(currentLocation); return list as Courtroom[]; } + async createCourtroom(courtroom: Partial): Promise { + return await this._client.CreateCourtroom(courtroom) as Courtroom; + } + + async updateCourtroom(courtroom: Partial): Promise { + const { id } = courtroom; + if (!id) { + throw 'No Id included in the courtroom to update'; + } + return await this._client.UpdateCourtroom(id, courtroom) as Courtroom; + } + + async deleteCourtroom(courtroomId: string): Promise { + return await this._client.DeleteCourtroom(courtroomId); + } + + /** + * TODO: We need a proper endpoint to deal with this this loop isn't gonna do it... + * @param ids + */ + async deleteCourtrooms(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteCourtroom(id)); + } + + return Promise.resolve(); + } + async getEscortRuns(): Promise { - const list = await this._client.GetEscortRuns(this.currentLocation); + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + + const list = await this._client.GetEscortRuns(currentLocation); return list as EscortRun[]; } @@ -407,6 +527,328 @@ export default class Client implements API { return list as GenderCode[]; } + // Methods for users + async getUsers(): Promise { + // TODO: Not sure if this is the best solution, but it gets things working they way we want to for now... + // ALL_LOCATIONS key is added to selectorValues in LocationSelector. + const currentLocation = (this.currentLocation && this.currentLocation !== 'ALL_LOCATIONS') + ? this.currentLocation + : undefined; + + const list = await this._client.GetUsersByLocationId(currentLocation); + return list as User[]; + } + + async getUser(id: IdType): Promise { + if (!id) { + throw 'No Id to request'; + } + return await this._client.GetUserById(id) as User; + } + + async createUser(user: Partial): Promise { + return await this._client.CreateUser(user) as User; + } + + async updateUser(user: Partial): Promise { + const { id } = user; + if (!id) { + throw 'No Id to request'; + } + return await this._client.GetUserById(id) as User; + } + + async deleteUser(userId: IdType): Promise { + return await this._client.DeleteUser(userId); + } + + async deleteUsers(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteUser(id)); + } + + return Promise.resolve(); + } + + // Methods for roles + async getRoles(): Promise { + const list = await this._client.GetRoles(); + return list as Role[]; + } + + async getRole(): Promise { + return {} as Role; + } + + async createRole(role: Partial): Promise { + return await this._client.CreateRole(role) as Role; + } + + async updateRole(role: Partial): Promise { + const { id } = role; + if (!id) { + throw 'No Id included in role to update'; + } + return await this._client.UpdateRole(id, role) as Role; + } + + // TODO: Add expireRole? or expireUserRole? + + async deleteRole(roleId: string): Promise { + return await this._client.DeleteRole(roleId); + } + + /** + * TODO: We need a proper endpoint to deal with this this loop isn't gonna do it... + * @param ids + */ + async deleteRoles(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteRole(id)); + } + + return Promise.resolve(); + } + + async getRolePermissions(): Promise { + const list = await this._client.GetRolePermissions(); + return list as RolePermission[]; + } + + async getRolePermission(): Promise { + return {} as RolePermission; + } + + async createRolePermission(rolePermission: Partial): Promise { + return await this._client.CreateRolePermission(rolePermission) as RolePermission; + } + + async updateRolePermission(rolePermission: Partial): Promise { + const { id } = rolePermission; + if (!id) { + throw 'No Id included in the rolePermission to update'; + } + return await this._client.UpdateRolePermission(id, rolePermission) as RolePermission; + } + + async deleteRolePermission(permissionId: IdType): Promise { + return await this._client.DeleteRolePermission(permissionId); + } + + async deleteRolePermissions(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteRolePermission(id)); + } + + return Promise.resolve(); + } + + async getFrontendScopes(): Promise { + const list = await this._client.GetFrontendScopes(); + return list as FrontendScope[]; + } + + async getFrontendScope(): Promise { + return {} as FrontendScope; + } + + async createFrontendScope(frontendScope: Partial): Promise { + return this._client.CreateFrontendScope(frontendScope) as FrontendScope; + } + + async updateFrontendScope(frontendScope: Partial): Promise { + const { id } = frontendScope; + if (!id) { + throw 'No Id included in the frontendScope to update'; + } + return this._client.UpdateFrontendScope(id, frontendScope) as FrontendScope; + } + + async deleteFrontendScope(frontendScopeId: IdType): Promise { + return this._client.DeleteFrontendScope(frontendScopeId); + } + + async deleteFrontendScopes(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteFrontendScope(id)); + } + + return Promise.resolve(); + } + + async getFrontendScopePermissions(): Promise { + const list = await this._client.GetFrontendScopePermissions(); + return list as FrontendScopePermission[]; + } + + async getFrontendScopePermission(): Promise { + return {} as FrontendScopePermission; + } + + async createFrontendScopePermission(permission: FrontendScopePermission): Promise { + return this._client.CreateFrontendScopePermission(permission) as FrontendScopePermission; + } + + async updateFrontendScopePermission(permission: FrontendScopePermission): Promise { + const { id } = permission; + if (!id) { + throw 'No Id included in the frontendScopePermission to update'; + } + return this._client.UpdateFrontendScopePermission(id, permission) as FrontendScopePermission; + } + + async deleteFrontendScopePermission(permissionId: IdType): Promise { + return await this._client.DeleteFrontendScopePermission(permissionId); + } + + async deleteFrontendScopePermissions(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteFrontendScopePermission(id)); + } + + return Promise.resolve(); + } + + async getApiScopes(): Promise { + const list = await this._client.GetApiScopes(); + return list as ApiScope[]; + } + + async getApiScope(): Promise { + return {} as ApiScope; + } + + async createApiScope(apiScope: Partial): Promise { + return this._client.CreateApiScope(apiScope) as ApiScope; + } + + async updateApiScope(apiScope: Partial): Promise { + const { id } = apiScope; + if (!id) { + throw 'No Id included in the apiScope to update'; + } + return this._client.UpdateApiScope(id, apiScope) as ApiScope; + } + + async deleteApiScope(frontendScopeId: IdType): Promise { + return this._client.DeleteApiScope(frontendScopeId); + } + + async deleteApiScopes(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteApiScope(id)); + } + + return Promise.resolve(); + } + + async getRoleFrontendScopes(): Promise { + const list = await this._client.GetRoleFrontendScopes(); + return list as RoleFrontendScope[]; + } + + async getRoleFrontendScope(): Promise { + return {} as RoleFrontendScope; + } + + async createRoleFrontendScope(roleScope: Partial): Promise { + return await this._client.CreateRoleFrontendScope(roleScope) as RoleFrontendScope; + } + + async updateRoleFrontendScope(roleScope: Partial): Promise { + const { id } = roleScope; + if (!id) { + throw 'No Id included in the roleFrontendScope to update'; + } + return await this._client.UpdateRoleFrontendScope(id, roleScope) as RoleFrontendScope; + } + + async deleteRoleFrontendScope(roleScopeId: IdType): Promise { + return await this._client.DeleteRoleFrontendScope(roleScopeId); + } + + /** + * TODO: We need a proper endpoint to deal with this this loop isn't gonna do it... + * @param ids + */ + async deleteRoleFrontendScopes(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteRoleFrontendScope(id)); + } + + return Promise.resolve(); + } + + async getRoleApiScopes(): Promise { + const list = await this._client.GetRoleApiScopes(); + return list as RoleApiScope[]; + } + + async getRoleApiScope(): Promise { + return {} as RoleApiScope; + } + + async createRoleApiScope(roleScope: Partial): Promise { + return await this._client.CreateRoleApiScope(roleScope) as RoleApiScope; + } + + async updateRoleApiScope(roleScope: Partial): Promise { + const { id } = roleScope; + if (!id) { + throw 'No Id included in the roleApiScope to update'; + } + return await this._client.UpdateRoleFrontendScope(id, roleScope) as RoleApiScope; + } + + async deleteRoleApiScope(roleScopeId: IdType): Promise { + return await this._client.DeleteRoleApiScope(roleScopeId); + } + + /** + * TODO: We need a proper endpoint to deal with this this loop isn't gonna do it... + * @param ids + */ + async deleteRoleApiScopes(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteRoleApiScope(id)); + } + + return Promise.resolve(); + } + + async getUserRoles(): Promise { + const list = await this._client.GetUserRoles(); + return list as RoleApiScope[]; + } + + async getUserRole(): Promise { + return {} as UserRole; + } + + async createUserRole(userRole: UserRole): Promise { + return await this._client.CreateUserRole(userRole) as UserRole; + } + + async updateUserRole(userRole: UserRole): Promise { + const { id } = userRole; + if (!id) { + throw 'No Id included in the userRole to update'; + } + return await this._client.UpdateUserRole(id, userRole) as UserRole; + } + + async deleteUserRole(userRoleId: IdType): Promise { + return Promise.resolve(); + } + + async deleteUserRoles(ids: IdType[]): Promise { + if (ids.length > 0) { + ids.forEach(id => this._client.DeleteUserRole(id)); + } + + return Promise.resolve(); + } + // END! Methods for roles getToken(): Promise { return this._client.GetToken(); @@ -415,4 +857,4 @@ export default class Client implements API { return this._client.Logout(); } -} \ No newline at end of file +} diff --git a/src/app/api/index.ts b/src/app/api/index.ts index cf181836..77edfa03 100644 --- a/src/app/api/index.ts +++ b/src/app/api/index.ts @@ -1,5 +1,6 @@ export { Sheriff, + SheriffRank, Assignment, AssignmentMap, AssignmentDuty, @@ -16,8 +17,11 @@ export { Courtroom, API, IdType, + GenderCode, + MapType, Shift, Leave, + LeaveSubCode, SheriffDuty, TimeType, WorkSectionCode, @@ -29,6 +33,16 @@ export { AlternateAssignment, AlternateAssignmentMap, SheriffUnassignedRange, + User, + UserMap, + Role, + UserRole, + ApiScope, + FrontendScope, + FrontendScopePermission, + RoleApiScope, + RoleFrontendScope, + RolePermission, WORK_SECTIONS } from './Api'; diff --git a/src/app/assets/images/bc-logo-transparent-no-underline.png b/src/app/assets/images/bc-logo-transparent-no-underline.png new file mode 100644 index 00000000..abbfdac9 Binary files /dev/null and b/src/app/assets/images/bc-logo-transparent-no-underline.png differ diff --git a/src/app/components/AdminForm/AdminForm.css b/src/app/components/AdminForm/AdminForm.css new file mode 100644 index 00000000..8172546b --- /dev/null +++ b/src/app/components/AdminForm/AdminForm.css @@ -0,0 +1,47 @@ +/* Active Tab Content Pane */ +.sheriff-profile .tab-content>.active{ + padding-top:10px; +} + +/* Active Tab Content Pane, Table */ +.sheriff-profile .tab-content>.active table { + margin-top:0px; +} + +/* Active Tab Content Pane, Table */ +.sheriff-profile .tab-content>.active table tr:first-of-type>td, +.sheriff-profile .tab-content>.active table tr:first-of-type>td +{ + border-top-width: 0px; +} + +/* General Tab */ +.sheriff-profile .nav-tabs>li>a{ + color: #999; + background-color: #F8F8F8; + border-width: 1px; + border-color: #CCC; + border-style: solid; +} + +.sheriff-profile .nav-tabs>li>a:hover{ + color:#555; + background-color: #FAFAFA; +} + +/* First Tab */ +.sheriff-profile .nav-tabs>li:first-of-type>a{ + border-left-width:1px; +} + +/* Active Tab */ +.sheriff-profile .nav-tabs>li.active>a, +.sheriff-profile .nav-tabs>li.active>a:focus, +.sheriff-profile .nav-tabs>li.active>a:hover{ + color:#222; + /*border-color:#999; + border-left-width: 1px; + border-bottom-color:#DDD; */ + background-color:#FFF; + border-bottom-color:transparent; +} diff --git a/src/app/components/AdminForm/AdminForm.tsx b/src/app/components/AdminForm/AdminForm.tsx new file mode 100644 index 00000000..c9ad27fe --- /dev/null +++ b/src/app/components/AdminForm/AdminForm.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { IdType } from '../../api'; +import { FormContainer, FormContainerProps, FormContainerBase } from '../Form/FormContainer'; +import { InjectedFormProps } from 'redux-form'; +import { Tab, Row, Col, Nav, NavItem, Glyphicon } from 'react-bootstrap'; +import Form from '../FormElements/Form'; +import './AdminForm.css'; + +export interface AdminFormProps { + sheriffId?: IdType; // TODO: This doesn't need to be in here any more... only in user roles + isEditing?: boolean; + plugins?: FormContainerBase[]; + text?: string; + pluginsWithErrors?: { [key: string]: boolean }; + selectedSection?: string; + onSelectSection?: (sectionName: string) => void; + setPluginFilters?: Function; + showSheriffProfileModal?: (sheriffId: IdType, isEditing: boolean, sectionName?: string) => {}; + onSubmitSuccess?: () => void; + initialValues?: any; +} + +class AdminFormSectionNav extends React.PureComponent<{ title: string, hasErrors?: boolean }>{ + render() { + const { title, hasErrors } = this.props; + let className: string = ''; + let glyph: React.ReactNode; + if (hasErrors === true) { + className = 'text-danger'; + glyph = ; + } else if (hasErrors === false) { + className = 'text-success'; + glyph = ; + } + return ( + + {title} {glyph} + + ); + } +} + +export default class AdminForm extends React.PureComponent & AdminFormProps>{ + private handleSelectSection(sectionName: string) { + const { onSelectSection } = this.props; + if (onSelectSection) { + onSelectSection(sectionName); + } + } + + /*shouldComponentUpdate(nextProps: any, nextState: any){ + return true; + }*/ + + renderPlugin(plugin: FormContainer) { + const { initialValues = {}, isEditing = false, setPluginFilters, showSheriffProfileModal } = this.props; + + const pluginProps: FormContainerProps = { + // sheriffId, + data: initialValues[plugin.reduxFormKey], + setPluginFilters, + showSheriffProfileModal + }; + return isEditing + ? plugin.renderFormFields(pluginProps) + : plugin.renderDisplay(pluginProps); + } + + renderPlugins() { + const { + plugins = [], + pluginsWithErrors = {}, + } = this.props; + let { selectedSection } = this.props; + + const nonSectionPlugins = plugins.filter(p => !(p instanceof FormContainerBase)); + // tslint:disable-next-line:max-line-length + const sectionPlugins = plugins.filter(p => p instanceof FormContainerBase) as FormContainerBase[]; + selectedSection = selectedSection ? selectedSection : sectionPlugins[0] ? sectionPlugins[0].name : ''; // No selected key + return ( +
+ {nonSectionPlugins.map((p) => this.renderPlugin(p))} + this.handleSelectSection(key)} + activeKey={selectedSection} + > + + {sectionPlugins.length > 1 && ( + + + + )} + + + { + sectionPlugins + .map((p) => ( + + {this.renderPlugin(p)} + + )) + } + + + + +
+ ); + } + + renderForm() { + return ( +
+ {this.renderPlugins()} +
+ ); + } + + renderDisplay() { + return this.renderPlugins(); + } + + render() { + const { isEditing = false } = this.props; + return ( +
+ {isEditing ? this.renderForm() : this.renderDisplay()} +
+ ); + } +} diff --git a/src/app/components/AppVersionDisplay/AppVersionDisplay.tsx b/src/app/components/AppVersionDisplay/AppVersionDisplay.tsx index 032822f5..a08de105 100644 --- a/src/app/components/AppVersionDisplay/AppVersionDisplay.tsx +++ b/src/app/components/AppVersionDisplay/AppVersionDisplay.tsx @@ -8,7 +8,7 @@ export interface AppVersionDisplayProps { buildDate?: boolean; apiVersion?: boolean; apiCommitHash?: boolean; - style?: React.CSSProperties; + style?: any; // React.CSSProperties; } export default class AppVersionDisplay extends React.PureComponent { diff --git a/src/app/components/AssignmentDutyCard/AssignmentDutyCard.tsx b/src/app/components/AssignmentDutyCard/AssignmentDutyCard.tsx index 6ada0589..28f4f5fc 100644 --- a/src/app/components/AssignmentDutyCard/AssignmentDutyCard.tsx +++ b/src/app/components/AssignmentDutyCard/AssignmentDutyCard.tsx @@ -12,7 +12,7 @@ import AssignmentDutyInformationPanel from '../AssignmentDutyInformationPanel/As export interface AssignmentDutyCardProps { duty: AssignmentDuty; SheriffAssignmentRenderer?: React.ComponentType; - style?: React.CSSProperties; + style?: any; // React.CSSProperties; unassignedTimeRanges?: { [key: string]: SheriffUnassignedRange[] }; onDoubleClick?: () => void; onClick?: () => void; @@ -62,4 +62,4 @@ export default class AssignmentDutyCard extends React.PureComponent ); } -} \ No newline at end of file +} diff --git a/src/app/components/AssignmentScheduleCard.tsx b/src/app/components/AssignmentScheduleCard.tsx index 12d9381f..6c17dabb 100644 --- a/src/app/components/AssignmentScheduleCard.tsx +++ b/src/app/components/AssignmentScheduleCard.tsx @@ -35,7 +35,7 @@ export default class AssignmentScheduleCard extends React.Component; + style?: any; // React.CSSProperties; +} + +export default class CancelledPopover extends React.Component { + render() { + const { model, style} = this.props; + return ( + } + title={`${Leave.getLeaveTypeDisplay(model)} Cancelled`} + displayValue={ + + Date: {moment(model.cancelDate).format('MMM D, YYYY')}
+ Reason: +
+ } + /> + ); + } +} diff --git a/src/app/components/Form/FormContainer.tsx b/src/app/components/Form/FormContainer.tsx new file mode 100644 index 00000000..811690cf --- /dev/null +++ b/src/app/components/Form/FormContainer.tsx @@ -0,0 +1,229 @@ +import * as React from 'react'; +import { IdType } from '../../api'; +import { RootState } from '../../store'; +import { Dispatch } from 'redux'; +import { FormErrors } from 'redux-form'; +import { deletedDiff, detailedDiff } from 'deep-object-diff'; + +export interface FormContainerProps { + // TODO: We aren't really using objectId anymore, we should remove it... + // It's a remnant from converting the SheriffProfilePlugin to the current + // forms implementation... + objectId?: IdType; + data?: T; + setPluginFilters?: Function; + // TODO: It would be nice if we could somehow pass in showSheriffProfileModal some other way that was more declarative, and from the plugin... + // This is easy and works for now though. + showSheriffProfileModal?: (sheriffId: IdType, isEditing: boolean, sectionName?: string) => {}; +} +export interface FormContainer { + /** + * A unique plugin name, no spaces please eg. /[A-Za-z0-9_-]+/ + * @type {string} + * @memberof Form + */ + name: string; + /** + * The form key, used by redux-form, to bind the form instance to redux data slices + * @type {string} + * @memberof Form + */ + reduxFormKey: string; + renderDisplay(props: FormContainerProps): React.ReactNode; + renderFormFields(props: FormContainerProps): React.ReactNode; + hasErrors(errors: any): boolean; + // onSubmit(objectId: IdType | undefined, formValues: any, dispatch: Dispatch): Promise; + onSubmit(formValues: any, initialValues: any, dispatch: Dispatch): Promise; + fetchData(dispatch: Dispatch, filters: {} | undefined): void; + getData(state: RootState, filters?: {} | undefined): T | undefined; + validate(values: T): FormErrors | undefined; +} + +export abstract class FormContainerBase implements FormContainer { + /** + * A unique plugin name, no spaces please eg. /[A-Za-z0-9_-]+/ + * @type {string} + * @memberof Form + */ + abstract name: string; + /** + * The form key, used by redux-form, to bind the form instance to redux data slices + * @type {string} + * @memberof Form + */ + abstract reduxFormKey: string; + + abstract get title(): string; + + protected _dispatch?: Dispatch; + + public get dispatch(): Dispatch | undefined { + return this._dispatch; + } + + public set dispatch(dispatch: Dispatch | undefined) { + this._dispatch = dispatch; + } + + /** + * The formFieldNames are used to enhance to experience + * when submitting / saving the profile. These fields + * should be the names of fields used by this plugin + * to allow automatic determination of errors that + * exist within specific plugins (i.e. to highlight tabs + * with errors etc.) + * + * @abstract + * @type {{ [key: string]: string }} + * @memberof FormContainerBase + */ + abstract formFieldNames: { [key: string]: string }; + + // TODO: Might need to remove this, this isn't in the right place? + protected get filterFieldNames() { + const fieldNames = {}; + + Object.keys(this.formFieldNames) + .map(key => fieldNames[key] = `${this.formFieldNames[key]}_filters`); + + return fieldNames as { [key: string]: string }; + } + + DisplayComponent?: React.ReactType>; + FormComponent?: React.ReactType>; + + protected getDataFromFormValues(formValues: any, initialValues?: any) { + if (!initialValues) return formValues[this.reduxFormKey]; + + const initial = initialValues[this.reduxFormKey]; + const values = formValues[this.reduxFormKey]; + + const data: any = {}; + + const formKeys = Object.keys(this.formFieldNames); + // detailedDiff will return a diff object with added, deleted, and updated keys + // https://www.npmjs.com/package/deep-object-diff + const diffKeys = ['added', 'deleted', 'updated']; + formKeys.forEach(key => { + let isDirty = false; + const diff = detailedDiff(initial[key], values[key]); + diffKeys.forEach(diffKey => { + if (Object.keys(diff[diffKey]).length > 0) isDirty = true; + }); + + if (isDirty) data[key] = values[key]; + }); + + return data; + } + + protected mapDeletesFromFormValues(map: {}) { + return {}; + } + + protected getFilterData(filters: any) { + return Object.keys(this.filterFieldNames) + .reduce((data: any, filterKey: string, idx: number) => { + const dataKey = this.filterFieldNames[filterKey] + .split(`${this.reduxFormKey}.`).pop() as string; + + if (filters[filterKey]) data[dataKey] = filters[filterKey]; + + return data; + }, {}); + } + + protected getDataToDeleteFromFormValues(formValues: any, initialValues?: any) { + if (!initialValues) return formValues[this.reduxFormKey]; + + const initial = initialValues[this.reduxFormKey]; + const values = formValues[this.reduxFormKey]; + + let map: any = {}; + + // TODO: Use value, instead of key - redux-form is bound using the value + // We can check the path using containsPropertyPath which is on this class + const formKeys = Object.keys(this.formFieldNames); + formKeys.forEach(key => { + let isDirty = false; + const diff = deletedDiff(initial[key], values[key]); + if (Object.keys(diff).length > 0) isDirty = true; + + if (isDirty) map[key] = { initialValues: initial[key], values: values[key] }; + }); + + return this.mapDeletesFromFormValues(map); + } + + containsPropertyPath(errors: Object = {}, propertyPath: string = '') { + const propertyNames = propertyPath.split('.'); + let propertyError = errors; + if (propertyNames.length === 0) { + return false; + } + // Assume there is an error until proven innocent + let containsPath = true; + for (let i = 0; i < propertyNames.length; i++) { + const propertyName = propertyNames[i]; + if (!propertyError.hasOwnProperty(propertyName)) { + containsPath = false; + break; + } + + propertyError = propertyError[propertyName]; + } + // we've traversed the whole property string finding each piece, there is an error + return containsPath; + } + + renderDisplay(props: FormContainerProps): React.ReactNode { + const { DisplayComponent } = this; + return ( + DisplayComponent + ? + : ( +
+ FormContainer: DisplayComponent not set +
+ ) + ); + } + + renderFormFields(props: FormContainerProps): React.ReactNode { + const { FormComponent } = this; + return ( + FormComponent && + ); + } + + // async onSubmit(objectId: IdType | undefined, formValues: any, dispatch: Dispatch): Promise { + async onSubmit(formValues: any, initialValues: any, dispatch: Dispatch): Promise { + // does nothing + } + + hasErrors(errors: any) { + // Traverse first nodes of error object checking for errors on each + return Object.keys(errors).some(eKey => ( + Object.keys(this.formFieldNames).some(key => ( + this.containsPropertyPath(errors[eKey], this.formFieldNames[key]) + )) + )); + } + + fetchData(dispatch: Dispatch, filters: {} | undefined) { + // does nothing + } + + getData(state: RootState, filters?: {} | undefined): T | undefined { + // Does nothing + return undefined; + } + + validate(values: T): FormErrors | undefined { + return undefined; + } +} + +export abstract class FormContainerSectionPlugin extends FormContainerBase { + abstract get title(): string; +} diff --git a/src/app/components/FormElements/CheckboxField.tsx b/src/app/components/FormElements/CheckboxField.tsx index 8f9152b9..3bbc3ea5 100644 --- a/src/app/components/FormElements/CheckboxField.tsx +++ b/src/app/components/FormElements/CheckboxField.tsx @@ -5,13 +5,22 @@ import { default as FormFieldWrapper, FormFieldWrapperProps } from './FormFieldW export default class CheckboxField extends React.PureComponent { render() { const {input: {value, onChange}, label} = this.props; + return ( - - + + { + e.preventDefault(); + e.stopPropagation(); + // TODO: Something is making modals close on first click + // we might have to store activeModal ids in redux store + onChange(e); + }} + checked={value}> {label} ); } -} \ No newline at end of file +} diff --git a/src/app/components/FormElements/DateField.tsx b/src/app/components/FormElements/DateField.tsx index 0b334d96..f5ed65f7 100644 --- a/src/app/components/FormElements/DateField.tsx +++ b/src/app/components/FormElements/DateField.tsx @@ -29,9 +29,11 @@ export default class DateField extends React.PureComponent this.onChange(e)} - inputProps={{readOnly: true, placeholder: `Select ${label}` }} + // TODO: Why was this readonly? + // inputProps={{readOnly: true, placeholder: `Select ${label}` }} + inputProps={{placeholder: `Select ${label}` }} /> ); } -} \ No newline at end of file +} diff --git a/src/app/components/FormElements/FormFieldWrapper.tsx b/src/app/components/FormElements/FormFieldWrapper.tsx index 2494f3e2..20ea1220 100644 --- a/src/app/components/FormElements/FormFieldWrapper.tsx +++ b/src/app/components/FormElements/FormFieldWrapper.tsx @@ -4,6 +4,7 @@ import { WrappedFieldProps } from 'redux-form'; export interface FormFieldWrapperProps extends WrappedFieldProps { label?: string | React.ReactNode | any; + placeholder?: string | React.ReactNode | any; showLabel?: boolean; maxWidth?: number; fieldToolTip?: React.ReactNode; @@ -26,4 +27,4 @@ export default class FormFieldWrapper extends React.PureComponent ); } -} \ No newline at end of file +} diff --git a/src/app/components/FormElements/SelectorField.tsx b/src/app/components/FormElements/SelectorField.tsx index b0f0a5af..3ed03c69 100644 --- a/src/app/components/FormElements/SelectorField.tsx +++ b/src/app/components/FormElements/SelectorField.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { default as FormFieldWrapper, FormFieldWrapperProps } from './FormFieldWrapper'; export interface SelectorFieldProps extends FormFieldWrapperProps { - SelectorComponent: React.ComponentType<{ value: any, onChange: (v: any)=> void}>; + SelectorComponent: React.ComponentType<{ value: any, onChange: (v: any)=> void, disabled?: boolean}>; + disabled?: boolean; } export default class SelectorField extends React.PureComponent { @@ -11,12 +12,13 @@ export default class SelectorField extends React.PureComponent - + ); } -} \ No newline at end of file +} diff --git a/src/app/components/FormElements/SubmitButton.tsx b/src/app/components/FormElements/SubmitButton.tsx index c387adbf..b736e4eb 100644 --- a/src/app/components/FormElements/SubmitButton.tsx +++ b/src/app/components/FormElements/SubmitButton.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React from 'react'; import { Button } from 'react-bootstrap'; @@ -12,7 +12,9 @@ export interface SubmitButtonProps extends Partial { formName: string, submit?: () => void; onSubmit?: () => void; - style?: React.CSSProperties; + // TODO: Fix this... getting an error... + // Type 'React.CSSProperties | undefined' is not assignable to type 'React.CSSProperties | undefined'. Two different types with this name exist, but they are unrelated. + // style?: any; // React.CSSProperties; } class SubmitButton extends React.PureComponent{ @@ -40,4 +42,4 @@ const mapDispatchToProps = (dispatch: any, ownProps: SubmitButtonProps) => { }; }; -export default connect(null, mapDispatchToProps)(SubmitButton); \ No newline at end of file +export default connect(null, mapDispatchToProps)(SubmitButton); diff --git a/src/app/components/FormElements/TextArea.tsx b/src/app/components/FormElements/TextArea.tsx index 79796cd6..b3acea61 100644 --- a/src/app/components/FormElements/TextArea.tsx +++ b/src/app/components/FormElements/TextArea.tsx @@ -4,16 +4,19 @@ import { FormControl } from 'react-bootstrap'; export default class TextArea extends React.PureComponent{ render() { - const { input: { value, onChange }, label } = this.props; + const { input: { value, onChange }, label, placeholder } = this.props; + + const placeholderValue = (placeholder) ? placeholder : `Enter ${label}`; + return ( - ); } -} \ No newline at end of file +} diff --git a/src/app/components/FormElements/TextField.tsx b/src/app/components/FormElements/TextField.tsx index 5051029a..5bf8118c 100644 --- a/src/app/components/FormElements/TextField.tsx +++ b/src/app/components/FormElements/TextField.tsx @@ -4,11 +4,14 @@ import { FormControl } from 'react-bootstrap'; export default class TextField extends React.PureComponent{ render() { - const {input: {value, onChange}, label} = this.props; + const { input: {value, onChange}, label, placeholder } = this.props; + + const placeholderValue = (placeholder) ? placeholder : `Enter ${label}`; + return ( - + ); } -} \ No newline at end of file +} diff --git a/src/app/components/FormElements/TimePickerDropDownField.tsx b/src/app/components/FormElements/TimePickerDropDownField.tsx index 130d1fc7..46d7d69a 100644 --- a/src/app/components/FormElements/TimePickerDropDownField.tsx +++ b/src/app/components/FormElements/TimePickerDropDownField.tsx @@ -9,7 +9,7 @@ import TimePicker from './TimePicker'; interface TimePickerDropdownFieldProps { nullTimeLabel?: string; timeIncrement?: number; - style?: React.CSSProperties; + style?: any; // React.CSSProperties; } export default class TimePickerDropDownField extends @@ -71,4 +71,4 @@ export default class TimePickerDropDownField extends ); } -} \ No newline at end of file +} diff --git a/src/app/components/FormElements/TimeSliderCommon.tsx b/src/app/components/FormElements/TimeSliderCommon.tsx index be92e069..d617c04b 100644 --- a/src/app/components/FormElements/TimeSliderCommon.tsx +++ b/src/app/components/FormElements/TimeSliderCommon.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Tooltip from 'rc-tooltip'; -import { TimeType } from '../../api/Api'; +import { TimeType } from '../../api'; import Slider, { Handle, SliderProps } from 'rc-slider'; import 'rc-tooltip/assets/bootstrap.css'; import moment, { Moment } from 'moment'; @@ -9,7 +9,7 @@ export interface HandleWithTooltipProps { value: number; dragging: boolean; index: any; - overlayStyle?: React.CSSProperties; + overlayStyle?: any; // React.CSSProperties; overlayFormatter?: (value: number) => string; } @@ -43,7 +43,7 @@ export class HandleWithTooltip extends React.PureComponent} @@ -155,7 +155,7 @@ export interface TimeRangeProps { minTime: TimeType; maxTime: TimeType; timeIncrement?: number; - style?: React.CSSProperties; + style?: any; // React.CSSProperties; } /** @@ -257,4 +257,4 @@ export function sliderWithLimits

(ComponentToWrap: Reac } return WrappedSlider; -} \ No newline at end of file +} diff --git a/src/app/components/Icons/CircleIcon.tsx b/src/app/components/Icons/CircleIcon.tsx index 287a4493..6f180668 100644 --- a/src/app/components/Icons/CircleIcon.tsx +++ b/src/app/components/Icons/CircleIcon.tsx @@ -2,7 +2,7 @@ import React from 'react'; import './Icons.css'; export interface CircleIconProps { - style?: React.CSSProperties; + style?: any; // React.CSSProperties; } export default class CircleIcon extends React.PureComponent { render() { @@ -10,13 +10,13 @@ export default class CircleIcon extends React.PureComponent { style = {} } = this.props; return ( -

{
); } -} \ No newline at end of file +} diff --git a/src/app/components/LeaveCancelledPopover.tsx b/src/app/components/LeaveCancelledPopover.tsx index c837c4c2..5e9cdd9f 100644 --- a/src/app/components/LeaveCancelledPopover.tsx +++ b/src/app/components/LeaveCancelledPopover.tsx @@ -7,7 +7,7 @@ import LeaveCancelReasonCodeDisplay from '../containers/LeaveCancelReasonCodeDis export interface LeaveCancelledPopoverProps { leave: Partial; - style?: React.CSSProperties; + style?: any; // React.CSSProperties; } export default class LeaveCancelledPopover extends React.Component { diff --git a/src/app/components/Navigation.tsx b/src/app/components/Navigation.tsx index 206f1ba5..a0e9af21 100644 --- a/src/app/components/Navigation.tsx +++ b/src/app/components/Navigation.tsx @@ -2,13 +2,15 @@ import React from 'react'; import { Nav, Navbar, - NavbarBrand + NavbarBrand, + Badge, Glyphicon, Image } from 'react-bootstrap'; import NavigationLink from './NavigationLink'; -import LocationSelector from '../containers/LocationSelector'; +import LocationSelector from '../containers/NavLocationSelector'; import bcLogo from '../assets/images/bc-logo-transparent.png'; import bcLogoDark from '../assets/images/bc-logo-transparent-dark.png'; import NavigationDropDown from './NavigationDropDown'; +import avatarImg from '../assets/images/avatar.png'; export interface NavigationProps { @@ -36,13 +38,53 @@ export default class Navigation extends React.Component { label: 'Distribute Schedule' } }, - team: { - path: '/sheriffs/manage', - label: 'My Team' - }, assignment: { path: '/assignments/manage/add', label: 'Add Assignment' + }, + team: { + path: '/sheriffs/manage', + label: 'My Team', // TODO: Switch between 'Manage' and 'My' prefix depending on the role... + children: { + users: { + path: '/users/manage', + label: 'Manage Users' + }, + roles: { + path: '/roles/manage', + label: 'Define Roles & Access' + }, + userRoles: { + path: '/roles/assign', + label: 'Assign User Roles' + } + } + }, + courtrooms: { + path: '/courtrooms/manage', + label: 'Courtrooms' + }, + system: { + path: '#', + label: 'System Settings', + children: { + codes: { + path: '/codes/manage', + label: 'Configure Lists' + }, + components: { + path: '/components/manage', + label: 'Manage Components' + }, + apis: { + path: '/apis/manage', + label: 'Manage API Scopes' + } + } + }, + audit: { + path: '/audit', + label: 'Audit Records' } } @@ -55,12 +97,12 @@ export default class Navigation extends React.Component { - + Sheriff Scheduling System - +
); } -} \ No newline at end of file +} diff --git a/src/app/components/NavigationLink.tsx b/src/app/components/NavigationLink.tsx index 853f04d6..dd702e5c 100644 --- a/src/app/components/NavigationLink.tsx +++ b/src/app/components/NavigationLink.tsx @@ -25,7 +25,7 @@ class NavigationLink extends React.Component { } render() { - const { path, exactMatch = false, label } = this.props; + const { path, exactMatch = false, label, children } = this.props; return ( { onClick={(e) => this.handleClick(e)} > {label} + {children} } /> ); @@ -51,4 +52,4 @@ class NavigationLink extends React.Component { } } -export default NavigationLink; \ No newline at end of file +export default NavigationLink; diff --git a/src/app/components/Page/Page.tsx b/src/app/components/Page/Page.tsx index 678a04a7..553e47bc 100644 --- a/src/app/components/Page/Page.tsx +++ b/src/app/components/Page/Page.tsx @@ -2,15 +2,14 @@ import * as React from 'react'; import './Page.css'; export interface PageProps { - style?: React.CSSProperties; - toolbarStyle?: React.CSSProperties; - contentStyle?: React.CSSProperties; + style?: any; // React.CSSProperties; + toolbarStyle?: any; // React.CSSProperties; + contentStyle?: any; // React.CSSProperties; toolbar?: React.ReactNode; } - export interface PageToolbarProps { - style?: React.CSSProperties; + style?: any; // React.CSSProperties; left?: React.ReactNode; middle?: React.ReactNode; right?: React.ReactNode; @@ -23,10 +22,11 @@ class PageToolbar extends React.PureComponent{
diff --git a/src/app/components/SheriffDisplay.tsx b/src/app/components/SheriffDisplay.tsx index 466d4d46..cedd9884 100644 --- a/src/app/components/SheriffDisplay.tsx +++ b/src/app/components/SheriffDisplay.tsx @@ -3,7 +3,7 @@ import { Sheriff } from '../api/Api'; interface SheriffDisplayProps { sheriff?: Sheriff; - style?: React.CSSProperties; + style?: any; // React.CSSProperties; } export default class SheriffDisplay extends React.PureComponent { diff --git a/src/app/components/SheriffDutyBar/SheriffDutyBar.css b/src/app/components/SheriffDutyBar/SheriffDutyBar.css index 509fc13f..8d6fd9bb 100644 --- a/src/app/components/SheriffDutyBar/SheriffDutyBar.css +++ b/src/app/components/SheriffDutyBar/SheriffDutyBar.css @@ -1,5 +1,5 @@ .sheriff-duty-bar{ - height: 100%; + height: 100%; display: flex; align-content: stretch; align-items: stretch; @@ -28,7 +28,7 @@ background-color: rgba(255, 255, 255, .2); } -/* Don't Display the overlay if we click on the duty-bar */ +/* Don't TableColumnCellDisplay the overlay if we click on the duty-bar */ .sheriff-duty-bar:active::before{ display: none; -} \ No newline at end of file +} diff --git a/src/app/components/SheriffDutyBar/SheriffDutyBar.tsx b/src/app/components/SheriffDutyBar/SheriffDutyBar.tsx index 34814121..41f422d2 100644 --- a/src/app/components/SheriffDutyBar/SheriffDutyBar.tsx +++ b/src/app/components/SheriffDutyBar/SheriffDutyBar.tsx @@ -23,7 +23,7 @@ export interface SheriffDutyBarProps { onDropSheriff?: (sheriff: Sheriff, sheriffDuty: SheriffDuty) => void; onDropSheriffDuty?: (sourceSheriffDuty: SheriffDuty, targetSheriffDuty: SheriffDuty) => void; canDropSheriffDuty?: (sheriffDuty: SheriffDuty) => boolean; - style?: React.CSSProperties; + style?: any; // React.CSSProperties; computeStyle?: (status: { isActive: boolean, isOver: boolean, canDrop: boolean }) => React.CSSProperties; className?: string; } @@ -132,4 +132,4 @@ export default class SheriffDutyBar extends React.PureComponent ); } -} \ No newline at end of file +} diff --git a/src/app/components/SheriffProfile/SheriffProfile.css b/src/app/components/SheriffProfile/SheriffProfile.css index 5e868bd8..8172546b 100644 --- a/src/app/components/SheriffProfile/SheriffProfile.css +++ b/src/app/components/SheriffProfile/SheriffProfile.css @@ -3,12 +3,12 @@ padding-top:10px; } -/* Active Tab Content Pane, table */ +/* Active Tab Content Pane, Table */ .sheriff-profile .tab-content>.active table { margin-top:0px; } -/* Active Tab Content Pane, table */ +/* Active Tab Content Pane, Table */ .sheriff-profile .tab-content>.active table tr:first-of-type>td, .sheriff-profile .tab-content>.active table tr:first-of-type>td { @@ -34,9 +34,9 @@ border-left-width:1px; } -/* Active Tab */ +/* Active Tab */ .sheriff-profile .nav-tabs>li.active>a, -.sheriff-profile .nav-tabs>li.active>a:focus, +.sheriff-profile .nav-tabs>li.active>a:focus, .sheriff-profile .nav-tabs>li.active>a:hover{ color:#222; /*border-color:#999; diff --git a/src/app/components/SheriffProfile/SheriffProfile.tsx b/src/app/components/SheriffProfile/SheriffProfile.tsx index 0d330e36..fcda01f0 100644 --- a/src/app/components/SheriffProfile/SheriffProfile.tsx +++ b/src/app/components/SheriffProfile/SheriffProfile.tsx @@ -72,14 +72,17 @@ export default class SheriffProfile extends React.Component - {nonSectionPlugins.map((p) => this.renderPlugin(p))} + this.handleSelectSection(key)} activeKey={selectedSection} > - + + {nonSectionPlugins.map((p) => this.renderPlugin(p))} + +