From 4c9c7eac8c8fa5679e76e19d77aa36be80fa9e89 Mon Sep 17 00:00:00 2001 From: ruanhan1988 <2856197796@qq.com> Date: Mon, 19 Oct 2020 14:43:45 +0800 Subject: [PATCH 01/18] feat:(test) init schdule organization projects profile --- .../app/containers/Organizations/reducer.ts | 2 +- webapp/app/containers/Organizations/sagas.ts | 170 +++-- webapp/app/containers/Profile/actions.ts | 5 + webapp/app/containers/Profile/reducer.ts | 2 +- webapp/app/containers/Profile/selectors.ts | 2 +- webapp/app/containers/Projects/types.ts | 54 +- webapp/app/containers/Schedule/reducer.ts | 2 +- .../containers/Organizations/actions.test.ts | 720 ++++++++++++++++++ .../app/containers/Organizations/fixtures.ts | 82 ++ .../containers/Organizations/reducer.test.ts | 252 ++++++ .../app/containers/Organizations/saga.test.ts | 444 +++++++++++ .../Organizations/selectors.test.ts | 131 ++++ .../app/containers/Profile/actions.test.ts | 61 ++ .../test/app/containers/Profile/fixtures.ts | 29 + .../app/containers/Profile/reducer.test.ts | 56 ++ .../test/app/containers/Profile/sagas.test.ts | 52 ++ .../app/containers/Profile/selectors.test.ts | 50 ++ .../app/containers/Projects/actions.test.ts | 637 ++++++++++++++++ .../test/app/containers/Projects/fixtures.ts | 90 +++ .../app/containers/Projects/reducer.test.ts | 147 ++++ .../app/containers/Projects/sagas.test.ts | 389 ++++++++++ .../app/containers/Projects/selectors.test.ts | 84 ++ .../app/containers/Schedule/actions.test.ts | 281 +++++++ .../test/app/containers/Schedule/fixtures.ts | 77 ++ .../app/containers/Schedule/reducer.test.ts | 75 ++ .../app/containers/Schedule/sagas.test.ts | 187 +++++ .../app/containers/Schedule/selectors.test.ts | 77 ++ .../test/app/containers/Source/sagas.test.ts | 1 + webapp/test/utils/test-bundler.js | 2 +- 29 files changed, 4087 insertions(+), 74 deletions(-) create mode 100644 webapp/test/app/containers/Organizations/actions.test.ts create mode 100644 webapp/test/app/containers/Organizations/fixtures.ts create mode 100644 webapp/test/app/containers/Organizations/reducer.test.ts create mode 100644 webapp/test/app/containers/Organizations/saga.test.ts create mode 100644 webapp/test/app/containers/Organizations/selectors.test.ts create mode 100644 webapp/test/app/containers/Profile/actions.test.ts create mode 100644 webapp/test/app/containers/Profile/fixtures.ts create mode 100644 webapp/test/app/containers/Profile/reducer.test.ts create mode 100644 webapp/test/app/containers/Profile/sagas.test.ts create mode 100644 webapp/test/app/containers/Profile/selectors.test.ts create mode 100644 webapp/test/app/containers/Projects/actions.test.ts create mode 100644 webapp/test/app/containers/Projects/fixtures.ts create mode 100644 webapp/test/app/containers/Projects/reducer.test.ts create mode 100644 webapp/test/app/containers/Projects/sagas.test.ts create mode 100644 webapp/test/app/containers/Projects/selectors.test.ts create mode 100644 webapp/test/app/containers/Schedule/actions.test.ts create mode 100644 webapp/test/app/containers/Schedule/fixtures.ts create mode 100644 webapp/test/app/containers/Schedule/reducer.test.ts create mode 100644 webapp/test/app/containers/Schedule/sagas.test.ts create mode 100644 webapp/test/app/containers/Schedule/selectors.test.ts diff --git a/webapp/app/containers/Organizations/reducer.ts b/webapp/app/containers/Organizations/reducer.ts index 8fafc9d34..746a5af06 100644 --- a/webapp/app/containers/Organizations/reducer.ts +++ b/webapp/app/containers/Organizations/reducer.ts @@ -27,7 +27,7 @@ import { ActionTypes as ProjectActionTypes } from 'containers/Projects/constants import { OrganizationActionType } from './actions' import { ProjectActionType } from 'containers/Projects/actions' -const initialState: IOrganizationState = { +export const initialState: IOrganizationState = { organizations: [], currentOrganization: null, currentOrganizationLoading: false, diff --git a/webapp/app/containers/Organizations/sagas.ts b/webapp/app/containers/Organizations/sagas.ts index 2ee8f719b..40eafefa4 100644 --- a/webapp/app/containers/Organizations/sagas.ts +++ b/webapp/app/containers/Organizations/sagas.ts @@ -27,9 +27,8 @@ import { message } from 'antd' import request from 'utils/request' import api from 'utils/api' import { errorHandler } from 'utils/util' -import { resolveOnChange } from 'antd/lib/input/Input' -export function* getOrganizations () { +export function* getOrganizations() { try { const asyncData = yield call(request, api.organizations) const organizations = asyncData.payload @@ -40,8 +39,10 @@ export function* getOrganizations () { } } -export function* addOrganization (action: OrganizationActionType) { - if (action.type !== ActionTypes.ADD_ORGANIZATION) { return } +export function* addOrganization(action: OrganizationActionType) { + if (action.type !== ActionTypes.ADD_ORGANIZATION) { + return + } const { organization, resolve } = action.payload try { @@ -59,8 +60,10 @@ export function* addOrganization (action: OrganizationActionType) { } } -export function* editOrganization (action: OrganizationActionType) { - if (action.type !== ActionTypes.EDIT_ORGANIZATION) { return } +export function* editOrganization(action: OrganizationActionType) { + if (action.type !== ActionTypes.EDIT_ORGANIZATION) { + return + } const { organization } = action.payload try { @@ -77,8 +80,10 @@ export function* editOrganization (action: OrganizationActionType) { } } -export function* deleteOrganization (action: OrganizationActionType) { - if (action.type !== ActionTypes.DELETE_ORGANIZATION) { return } +export function* deleteOrganization(action: OrganizationActionType) { + if (action.type !== ActionTypes.DELETE_ORGANIZATION) { + return + } const { id, resolve } = action.payload try { @@ -94,11 +99,16 @@ export function* deleteOrganization (action: OrganizationActionType) { } } -export function* getOrganizationDetail (action: OrganizationActionType) { - if (action.type !== ActionTypes.LOAD_ORGANIZATION_DETAIL) { return } +export function* getOrganizationDetail(action: OrganizationActionType) { + if (action.type !== ActionTypes.LOAD_ORGANIZATION_DETAIL) { + return + } try { - const asyncData = yield call(request, `${api.organizations}/${action.payload.id}`) + const asyncData = yield call( + request, + `${api.organizations}/${action.payload.id}` + ) const organization = asyncData.payload yield put(OrganizationActions.organizationDetailLoaded(organization)) } catch (err) { @@ -106,13 +116,21 @@ export function* getOrganizationDetail (action: OrganizationActionType) { } } -export function* getOrganizationsProjects (action: OrganizationActionType) { - if (action.type !== ActionTypes.LOAD_ORGANIZATIONS_PROJECTS) { return } +export function* getOrganizationsProjects(action: OrganizationActionType) { + if (action.type !== ActionTypes.LOAD_ORGANIZATIONS_PROJECTS) { + return + } - const { param: { id, keyword, pageNum, pageSize } } = action.payload + const { + param: { id, keyword, pageNum, pageSize } + } = action.payload const requestUrl = keyword - ? `${api.organizations}/${id}/projects?keyword=${keyword}&pageNum=1&pageSize=${pageSize || 10}` - : `${api.organizations}/${id}/projects/?pageNum=${pageNum || 1}&pageSize=${pageSize || 10}` + ? `${ + api.organizations + }/${id}/projects?keyword=${keyword}&pageNum=1&pageSize=${pageSize || 10}` + : `${api.organizations}/${id}/projects/?pageNum=${pageNum || 1}&pageSize=${ + pageSize || 10 + }` try { const asyncData = yield call(request, { method: 'get', @@ -126,8 +144,10 @@ export function* getOrganizationsProjects (action: OrganizationActionType) { } } -export function* getOrganizationsMembers (action: OrganizationActionType) { - if (action.type !== ActionTypes.LOAD_ORGANIZATIONS_MEMBERS) { return } +export function* getOrganizationsMembers(action: OrganizationActionType) { + if (action.type !== ActionTypes.LOAD_ORGANIZATIONS_MEMBERS) { + return + } const { id } = action.payload try { @@ -139,8 +159,10 @@ export function* getOrganizationsMembers (action: OrganizationActionType) { } } -export function* getOrganizationsRole (action: OrganizationActionType) { - if (action.type !== ActionTypes.LOAD_ORGANIZATIONS_ROLE) { return } +export function* getOrganizationsRole(action: OrganizationActionType) { + if (action.type !== ActionTypes.LOAD_ORGANIZATIONS_ROLE) { + return + } const { id } = action.payload try { @@ -153,8 +175,10 @@ export function* getOrganizationsRole (action: OrganizationActionType) { } } -export function* addRole (action: OrganizationActionType) { - if (action.type !== ActionTypes.ADD_ROLE) { return } +export function* addRole(action: OrganizationActionType) { + if (action.type !== ActionTypes.ADD_ROLE) { + return + } const { name, description, id, resolve } = action.payload try { @@ -173,8 +197,10 @@ export function* addRole (action: OrganizationActionType) { } } -export function* getRoleListByMemberId (action: OrganizationActionType) { - if (action.type !== ActionTypes.GET_ROLELISTS_BY_MEMBERID) { return } +export function* getRoleListByMemberId(action: OrganizationActionType) { + if (action.type !== ActionTypes.GET_ROLELISTS_BY_MEMBERID) { + return + } const { memberId, orgId, resolve } = action.payload try { const asyncData = yield call(request, { @@ -182,7 +208,9 @@ export function* getRoleListByMemberId (action: OrganizationActionType) { url: `${api.organizations}/${orgId}/member/${memberId}/roles` }) const result = asyncData.payload - yield put(OrganizationActions.getRoleListByMemberIdSuccess(result, memberId)) + yield put( + OrganizationActions.getRoleListByMemberIdSuccess(result, memberId) + ) if (resolve) { resolve(result) } @@ -192,8 +220,10 @@ export function* getRoleListByMemberId (action: OrganizationActionType) { } } -export function* deleteRole (action: OrganizationActionType) { - if (action.type !== ActionTypes.DELETE_ROLE) { return } +export function* deleteRole(action: OrganizationActionType) { + if (action.type !== ActionTypes.DELETE_ROLE) { + return + } const { id, resolve } = action.payload try { @@ -210,12 +240,14 @@ export function* deleteRole (action: OrganizationActionType) { } } -export function* editRole (action: OrganizationActionType) { - if (action.type !== ActionTypes.EDIT_ROLE) { return } +export function* editRole(action: OrganizationActionType) { + if (action.type !== ActionTypes.EDIT_ROLE) { + return + } const { name, description, id, resolve } = action.payload try { - const role = {name, description} + const role = { name, description } const asyncData = yield call(request, { method: 'put', url: `${api.roles}/${id}`, @@ -230,8 +262,10 @@ export function* editRole (action: OrganizationActionType) { } } -export function* relRoleMember (action: OrganizationActionType) { - if (action.type !== ActionTypes.REL_ROLE_MEMBER) { return } +export function* relRoleMember(action: OrganizationActionType) { + if (action.type !== ActionTypes.REL_ROLE_MEMBER) { + return + } const { id, memberIds, resolve } = action.payload try { @@ -248,8 +282,10 @@ export function* relRoleMember (action: OrganizationActionType) { } } -export function* getRelRoleMember (action: OrganizationActionType) { - if (action.type !== ActionTypes.GET_REL_ROLE_MEMBER) { return } +export function* getRelRoleMember(action: OrganizationActionType) { + if (action.type !== ActionTypes.GET_REL_ROLE_MEMBER) { + return + } const { id, resolve } = action.payload try { @@ -266,8 +302,10 @@ export function* getRelRoleMember (action: OrganizationActionType) { } } -export function* searchMember (action: OrganizationActionType) { - if (action.type !== ActionTypes.SEARCH_MEMBER) { return } +export function* searchMember(action: OrganizationActionType) { + if (action.type !== ActionTypes.SEARCH_MEMBER) { + return + } const { keyword } = action.payload try { @@ -284,8 +322,10 @@ export function* searchMember (action: OrganizationActionType) { } } -export function* inviteMember (action: OrganizationActionType) { - if (action.type !== ActionTypes.INVITE_MEMBER) { return } +export function* inviteMember(action: OrganizationActionType) { + if (action.type !== ActionTypes.INVITE_MEMBER) { + return + } const { orgId, members, needEmail, resolve } = action.payload try { @@ -309,8 +349,10 @@ export function* inviteMember (action: OrganizationActionType) { } } -export function* deleteOrganizationMember (action: OrganizationActionType) { - if (action.type !== ActionTypes.DELETE_ORGANIZATION_MEMBER) { return } +export function* deleteOrganizationMember(action: OrganizationActionType) { + if (action.type !== ActionTypes.DELETE_ORGANIZATION_MEMBER) { + return + } const { relationId, resolve } = action.payload try { @@ -326,18 +368,22 @@ export function* deleteOrganizationMember (action: OrganizationActionType) { } } -export function* changeOrganizationMemberRole (action: OrganizationActionType) { - if (action.type !== ActionTypes.CHANGE_MEMBER_ROLE_ORGANIZATION) { return } +export function* changeOrganizationMemberRole(action: OrganizationActionType) { + if (action.type !== ActionTypes.CHANGE_MEMBER_ROLE_ORGANIZATION) { + return + } const { relationId, newRole, resolve } = action.payload try { const asyncData = yield call(request, { url: `${api.organizations}/member/${relationId}`, method: 'put', - data: {role: newRole} + data: { role: newRole } }) const member = asyncData.payload - yield put(OrganizationActions.organizationMemberRoleChanged(relationId, member)) + yield put( + OrganizationActions.organizationMemberRoleChanged(relationId, member) + ) yield resolve() } catch (err) { yield put(OrganizationActions.changeOrganizationMemberRoleFail()) @@ -345,8 +391,10 @@ export function* changeOrganizationMemberRole (action: OrganizationActionType) { } } -export function* getProjectAdmins (action: OrganizationActionType) { - if (action.type !== ActionTypes.LOAD_PROJECT_ADMINS) { return } +export function* getProjectAdmins(action: OrganizationActionType) { + if (action.type !== ActionTypes.LOAD_PROJECT_ADMINS) { + return + } const { projectId } = action.payload try { @@ -359,8 +407,10 @@ export function* getProjectAdmins (action: OrganizationActionType) { } } -export function* getVizVisbility (action: OrganizationActionType) { - if (action.type !== ActionTypes.GET_VIZ_VISBILITY) { return } +export function* getVizVisbility(action: OrganizationActionType) { + if (action.type !== ActionTypes.GET_VIZ_VISBILITY) { + return + } const { roleId, projectId, resolve } = action.payload try { @@ -375,8 +425,10 @@ export function* getVizVisbility (action: OrganizationActionType) { } } -export function* postVizVisbility (action: OrganizationActionType) { - if (action.type !== ActionTypes.POST_VIZ_VISBILITY) { return } +export function* postVizVisbility(action: OrganizationActionType) { + if (action.type !== ActionTypes.POST_VIZ_VISBILITY) { + return + } const { id, permission, resolve } = action.payload try { @@ -392,8 +444,7 @@ export function* postVizVisbility (action: OrganizationActionType) { } } - -export default function* rootOrganizationSaga () { +export default function* rootOrganizationSaga() { yield all([ takeLatest(ActionTypes.LOAD_ORGANIZATIONS, getOrganizations), takeEvery(ActionTypes.ADD_ORGANIZATION, addOrganization), @@ -401,7 +452,10 @@ export default function* rootOrganizationSaga () { takeEvery(ActionTypes.DELETE_ORGANIZATION, deleteOrganization), takeLatest(ActionTypes.LOAD_ORGANIZATION_DETAIL, getOrganizationDetail), takeLatest(ActionTypes.LOAD_ORGANIZATIONS_MEMBERS, getOrganizationsMembers), - takeLatest(ActionTypes.LOAD_ORGANIZATIONS_PROJECTS, getOrganizationsProjects), + takeLatest( + ActionTypes.LOAD_ORGANIZATIONS_PROJECTS, + getOrganizationsProjects + ), takeLatest(ActionTypes.LOAD_ORGANIZATIONS_ROLE, getOrganizationsRole), takeEvery(ActionTypes.ADD_ROLE, addRole), takeEvery(ActionTypes.DELETE_ROLE, deleteRole), @@ -411,8 +465,14 @@ export default function* rootOrganizationSaga () { takeLatest(ActionTypes.LOAD_PROJECT_ADMINS, getProjectAdmins), takeLatest(ActionTypes.INVITE_MEMBER, inviteMember), takeLatest(ActionTypes.SEARCH_MEMBER, searchMember), - takeLatest(ActionTypes.DELETE_ORGANIZATION_MEMBER, deleteOrganizationMember), - takeLatest(ActionTypes.CHANGE_MEMBER_ROLE_ORGANIZATION, changeOrganizationMemberRole), + takeLatest( + ActionTypes.DELETE_ORGANIZATION_MEMBER, + deleteOrganizationMember + ), + takeLatest( + ActionTypes.CHANGE_MEMBER_ROLE_ORGANIZATION, + changeOrganizationMemberRole + ), takeLatest(ActionTypes.GET_VIZ_VISBILITY, getVizVisbility), takeLatest(ActionTypes.POST_VIZ_VISBILITY, postVizVisbility), takeEvery(ActionTypes.GET_ROLELISTS_BY_MEMBERID, getRoleListByMemberId) diff --git a/webapp/app/containers/Profile/actions.ts b/webapp/app/containers/Profile/actions.ts index bee64b638..fea643367 100644 --- a/webapp/app/containers/Profile/actions.ts +++ b/webapp/app/containers/Profile/actions.ts @@ -48,3 +48,8 @@ export function getUserProfileFail () { } } +export default { + getUserProfile, + userProfileGot, + getUserProfileFail +} diff --git a/webapp/app/containers/Profile/reducer.ts b/webapp/app/containers/Profile/reducer.ts index 15d93bc70..ed41e5a4f 100644 --- a/webapp/app/containers/Profile/reducer.ts +++ b/webapp/app/containers/Profile/reducer.ts @@ -25,7 +25,7 @@ import { } from './constants' -const initialState = { +export const initialState = { userProfile: false, loading: false } diff --git a/webapp/app/containers/Profile/selectors.ts b/webapp/app/containers/Profile/selectors.ts index 6a5a45630..ae2ef5c46 100644 --- a/webapp/app/containers/Profile/selectors.ts +++ b/webapp/app/containers/Profile/selectors.ts @@ -20,7 +20,7 @@ import { createSelector } from 'reselect' -const selectProfile = (state) => state.profile +export const selectProfile = (state) => state.profile const makeSelectUserProfile = () => createSelector( selectProfile, diff --git a/webapp/app/containers/Projects/types.ts b/webapp/app/containers/Projects/types.ts index 963403dc5..601760b4f 100644 --- a/webapp/app/containers/Projects/types.ts +++ b/webapp/app/containers/Projects/types.ts @@ -17,7 +17,7 @@ * limitations under the License. * >> */ -import {IOrganization} from '../Organizations/types' +import { IOrganization } from '../Organizations/types' export interface IProjectPermission { downloadPermission: boolean @@ -30,7 +30,7 @@ export interface IProjectPermission { } export interface IProject { - createBy?: { avatar?: string, id?: number, username?: string, email: string} + createBy?: { avatar?: string; id?: number; username?: string; email: string } permission?: IProjectPermission initialOrgId?: number userId: number @@ -50,11 +50,12 @@ export interface IProject { export interface IStarUser { avatar: string id: number + email?: string starTime: string username: string } -interface IProjectRole { +export interface IProjectRole { id: number name: string description: string @@ -75,7 +76,6 @@ export interface IProjectState { projectRoles: IProjectRole[] } - export interface IProjectFormFieldProps { id?: number orgId_hc?: string @@ -93,19 +93,28 @@ export interface IProjectsFormProps { onModalOk?: () => any modalLoading: boolean currentPro: Partial - onCheckUniqueName?: (pathname: any, data: any, resolve: () => any, reject: (error: string) => any) => any + onCheckUniqueName?: ( + pathname: any, + data: any, + resolve: () => any, + reject: (error: string) => any + ) => any } export interface IEnhanceButtonProps { type?: string } - export interface IProjectsProps { projects: IProject[] collectProjects: IProject[] loginUser: any - searchProject?: {list: any[], total: number, pageNum: number, pageSize: number} + searchProject?: { + list: any[] + total: number + pageNum: number + pageSize: number + } organizations: IOrganization[] starUserList: IStarUser[] onTransferProject: (id: number, orgId: number) => any @@ -114,13 +123,26 @@ export interface IProjectsProps { onAddProject: (project: any, resolve: () => any) => any onLoadOrganizations: () => any onLoadCollectProjects: () => any - onClickCollectProjects: (isFavorite: boolean, proId: number, resolve: (id: number) => any) => any + onClickCollectProjects: ( + isFavorite: boolean, + proId: number, + resolve: (id: number) => any + ) => any onDeleteProject: (id: number, resolve?: any) => any onLoadProjectDetail: (id: number) => any - onStarProject: (id: number, resolve: () => any) => any, - onGetProjectStarUser: (id: number) => any, - onSearchProject: (param: {keywords: string, pageNum: number, pageSize: number }) => any - onCheckUniqueName: (pathname: any, data: any, resolve: () => any, reject: (error: string) => any) => any + onStarProject: (id: number, resolve: () => any) => any + onGetProjectStarUser: (id: number) => any + onSearchProject: (param: { + keywords: string + pageNum: number + pageSize: number + }) => any + onCheckUniqueName: ( + pathname: any, + data: any, + resolve: () => any, + reject: (error: string) => any + ) => any } export enum projectType { @@ -166,7 +188,7 @@ export interface ItemToolbarProps { } export interface ITagProps { - type: Array<'create'|'favorite'|'join'> + type: Array<'create' | 'favorite' | 'join'> } export enum eTag { @@ -187,7 +209,11 @@ export interface ItemProps { favoritePro?: IFavoritePro } -export type IShowProForm = (formType: string, project: Partial, e: React.MouseEvent) => void +export type IShowProForm = ( + formType: string, + project: Partial, + e: React.MouseEvent +) => void export interface IContentProps { userId: number pType: string diff --git a/webapp/app/containers/Schedule/reducer.ts b/webapp/app/containers/Schedule/reducer.ts index e1ea0f0ee..1f84ac907 100644 --- a/webapp/app/containers/Schedule/reducer.ts +++ b/webapp/app/containers/Schedule/reducer.ts @@ -22,7 +22,7 @@ import produce from 'immer' import { ActionTypes, EmptySchedule, EmptyWeChatWorkSchedule } from './constants' import { ScheduleActionType } from './actions' -const initialState = { +export const initialState = { schedules: [], editingSchedule: EmptySchedule, loading: { diff --git a/webapp/test/app/containers/Organizations/actions.test.ts b/webapp/test/app/containers/Organizations/actions.test.ts new file mode 100644 index 000000000..476876735 --- /dev/null +++ b/webapp/test/app/containers/Organizations/actions.test.ts @@ -0,0 +1,720 @@ +import { ActionTypes } from 'app/containers/Organizations/constants' +import actions from 'app/containers/Organizations/actions' +import { mockStore } from './fixtures' + +describe('Organizations Actions', () => { + const { + orgId, + projects, + member, + members, + role, + roles, + resolve, + organization, + organizations + } = mockStore + describe('loadOrganizationProjects', () => { + it(' loadOrganizationProjects should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_PROJECTS, + payload: { + param: orgId + } + } + expect(actions.loadOrganizationProjects(orgId)).toEqual(expectedResult) + }) + }) + describe('organizationsProjectsLoaded', () => { + it('organizationsProjectsLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_PROJECTS_SUCCESS, + payload: { + projects + } + } + expect(actions.organizationsProjectsLoaded(projects)).toEqual( + expectedResult + ) + }) + }) + describe('loadOrganizationsProjectsFail', () => { + it('loadOrganizationsProjectsFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_PROJECTS_FAILURE + } + expect(actions.loadOrganizationsProjectsFail()).toEqual(expectedResult) + }) + }) + describe('loadOrganizationMembers', () => { + it('loadOrganizationMembers should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_MEMBERS, + payload: { + id: orgId + } + } + expect(actions.loadOrganizationMembers(orgId)).toEqual(expectedResult) + }) + }) + describe('organizationsMembersLoaded', () => { + it('organizationsMembersLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_MEMBERS_SUCCESS, + payload: { + members + } + } + expect(actions.organizationsMembersLoaded(members)).toEqual( + expectedResult + ) + }) + }) + describe('loadOrganizationsMembersFail', () => { + it('loadOrganizationsMembersFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_MEMBERS_FAILURE + } + expect(actions.loadOrganizationsMembersFail()).toEqual(expectedResult) + }) + }) + describe('loadOrganizationRole', () => { + it('loadOrganizationRole should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_ROLE, + payload: { + id: orgId + } + } + expect(actions.loadOrganizationRole(orgId)).toEqual(expectedResult) + }) + }) + describe('organizationsRoleLoaded', () => { + it('organizationsRoleLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_ROLE_SUCCESS, + payload: { + role + } + } + expect(actions.organizationsRoleLoaded(role)).toEqual(expectedResult) + }) + }) + describe('loadOrganizationsRoleFail', () => { + it('loadOrganizationsRoleFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_ROLE_FAILURE, + payload: {} + } + expect(actions.loadOrganizationsRoleFail()).toEqual(expectedResult) + }) + }) + describe('loadOrganizations', () => { + it('loadOrganizations should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS + } + expect(actions.loadOrganizations()).toEqual(expectedResult) + }) + }) + describe('organizationsLoaded', () => { + it('organizationsLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_SUCCESS, + payload: { + organizations + } + } + expect(actions.organizationsLoaded(organizations)).toEqual(expectedResult) + }) + }) + describe('loadOrganizationsFail', () => { + it('loadOrganizationsFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATIONS_FAILURE, + payload: {} + } + expect(actions.loadOrganizationsFail()).toEqual(expectedResult) + }) + }) + describe('addOrganization', () => { + it('addOrganization should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_ORGANIZATION, + payload: { + organization, + resolve + } + } + expect(actions.addOrganization(organization, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('organizationAdded', () => { + it('organizationAdded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_ORGANIZATION_SUCCESS, + payload: { + result: organization + } + } + expect(actions.organizationAdded(organization)).toEqual(expectedResult) + }) + }) + describe('addOrganizationFail', () => { + it('addOrganizationFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_ORGANIZATION_FAILURE, + payload: {} + } + expect(actions.addOrganizationFail()).toEqual(expectedResult) + }) + }) + describe('editOrganization', () => { + it('editOrganization should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_ORGANIZATION, + payload: { organization } + } + expect(actions.editOrganization(organization)).toEqual(expectedResult) + }) + }) + describe('organizationEdited', () => { + it('organizationEdited should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_ORGANIZATION_SUCCESS, + payload: { + result: organization + } + } + expect(actions.organizationEdited(organization)).toEqual(expectedResult) + }) + }) + describe('editOrganizationFail', () => { + it('editOrganizationFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_ORGANIZATION_FAILURE, + payload: {} + } + expect(actions.editOrganizationFail()).toEqual(expectedResult) + }) + }) + describe('deleteOrganization', () => { + it('deleteOrganization should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ORGANIZATION, + payload: { + id: orgId, + resolve + } + } + expect(actions.deleteOrganization(orgId, resolve)).toEqual(expectedResult) + }) + }) + describe('organizationDeleted', () => { + it('organizationDeleted should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ORGANIZATION_SUCCESS, + payload: { + id: orgId + } + } + expect(actions.organizationDeleted(orgId)).toEqual(expectedResult) + }) + }) + describe('deleteOrganizationFail', () => { + it('deleteOrganizationFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ORGANIZATION_FAILURE, + payload: {} + } + expect(actions.deleteOrganizationFail()).toEqual(expectedResult) + }) + }) + describe('loadOrganizationDetail', () => { + it('loadOrganizationDetail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATION_DETAIL, + payload: { + id: orgId + } + } + expect(actions.loadOrganizationDetail(orgId)).toEqual(expectedResult) + }) + }) + describe('organizationDetailLoaded', () => { + it('organizationDetailLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATION_DETAIL_SUCCESS, + payload: { organization } + } + expect(actions.organizationDetailLoaded(organization)).toEqual( + expectedResult + ) + }) + }) + describe('loadOrganizationDetailFail', () => { + it('loadOrganizationDetailFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_ORGANIZATION_DETAIL_FAILURE, + payload: { + organization, + widgets: '' + } + } + expect(actions.loadOrganizationDetailFail(organization, '')).toEqual( + expectedResult + ) + }) + }) + describe('addRole', () => { + it('addRole should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_ROLE, + payload: { + name: role.name, + id: role.id, + description: role.description, + resolve + } + } + expect( + actions.addRole(role.name, role.description, role.id, resolve) + ).toEqual(expectedResult) + }) + }) + describe('roleAdded', () => { + it('roleAdded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_ROLE_SUCCESS, + payload: { + result: role + } + } + expect(actions.roleAdded(role)).toEqual(expectedResult) + }) + }) + describe('addRoleFail', () => { + it('addRoleFail should return the correct type', () => { + const expectedResult = { type: ActionTypes.ADD_ROLE_FAILURE, payload: {} } + expect(actions.addRoleFail()).toEqual(expectedResult) + }) + }) + describe('deleteRole', () => { + it('deleteRole should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ROLE, + payload: { + id: orgId, + resolve + } + } + expect(actions.deleteRole(orgId, resolve)).toEqual(expectedResult) + }) + }) + describe('roleDeleted', () => { + it('roleDeleted should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ROLE_SUCCESS, + payload: { + result: role + } + } + expect(actions.roleDeleted(role)).toEqual(expectedResult) + }) + }) + describe('deleteRoleFail', () => { + it('deleteRoleFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ROLE_FAILURE, + payload: {} + } + expect(actions.deleteRoleFail()).toEqual(expectedResult) + }) + }) + describe('editRole', () => { + it('editRole should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_ROLE, + payload: { + name: role.name, + id: role.id, + description: role.description, + resolve + } + } + expect( + actions.editRole(role.name, role.description, role.id, resolve) + ).toEqual(expectedResult) + }) + }) + describe('roleEdited', () => { + it('roleEdited should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_ROLE_SUCCESS, + payload: { + result: role + } + } + expect(actions.roleEdited(role)).toEqual(expectedResult) + }) + }) + describe('editRoleFail', () => { + it('editRoleFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_ROLE_FAILURE, + payload: {} + } + expect(actions.editRoleFail()).toEqual(expectedResult) + }) + }) + describe('searchMember', () => { + it('searchMember should return the correct type', () => { + const expectedResult = { + type: ActionTypes.SEARCH_MEMBER, + payload: { + keyword: '' + } + } + expect(actions.searchMember('')).toEqual(expectedResult) + }) + }) + describe('memberSearched', () => { + it('memberSearched should return the correct type', () => { + const expectedResult = { + type: ActionTypes.SEARCH_MEMBER_SUCCESS, + payload: { + result: member + } + } + expect(actions.memberSearched(member)).toEqual(expectedResult) + }) + }) + describe('searchMemberFail', () => { + it('searchMemberFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.SEARCH_MEMBER_FAILURE, + payload: {} + } + expect(actions.searchMemberFail()).toEqual(expectedResult) + }) + }) + describe('inviteMember', () => { + it('inviteMember should return the correct type', () => { + const expectedResult = { + type: ActionTypes.INVITE_MEMBER, + payload: { + orgId, + members, + needEmail: false, + resolve + } + } + expect(actions.inviteMember(orgId, members, false, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('inviteMemberSuccess', () => { + it('inviteMemberSuccess should return the correct type', () => { + const expectedResult = { + type: ActionTypes.INVITE_MEMBER_SUCCESS, + payload: { + result: member + } + } + expect(actions.inviteMemberSuccess(member)).toEqual(expectedResult) + }) + }) + describe('inviteMemberFail', () => { + it('inviteMemberFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.INVITE_MEMBER_FAILURE, + payload: {} + } + expect(actions.inviteMemberFail()).toEqual(expectedResult) + }) + }) + describe('deleteOrganizationMember', () => { + it('deleteOrganizationMember should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ORGANIZATION_MEMBER, + payload: { + relationId: orgId, + resolve + } + } + expect(actions.deleteOrganizationMember(orgId, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('organizationMemberDeleted', () => { + it('organizationMemberDeleted should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ORGANIZATION_MEMBER_SUCCESS, + payload: { + id: orgId + } + } + expect(actions.organizationMemberDeleted(orgId)).toEqual(expectedResult) + }) + }) + describe('deleteOrganizationMemberFail', () => { + it('deleteOrganizationMemberFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_ORGANIZATION_MEMBER_ERROR, + payload: {} + } + expect(actions.deleteOrganizationMemberFail()).toEqual(expectedResult) + }) + }) + describe('changeOrganizationMemberRole', () => { + it('changeOrganizationMemberRole should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CHANGE_MEMBER_ROLE_ORGANIZATION, + payload: { + relationId: orgId, + newRole: role, + resolve + } + } + expect( + actions.changeOrganizationMemberRole(orgId, role, resolve) + ).toEqual(expectedResult) + }) + }) + describe('organizationMemberRoleChanged', () => { + it('organizationMemberRoleChanged should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CHANGE_MEMBER_ROLE_ORGANIZATION_SUCCESS, + payload: { + relationId: orgId, + newRole: role + } + } + expect(actions.organizationMemberRoleChanged(orgId, role)).toEqual( + expectedResult + ) + }) + }) + describe('changeOrganizationMemberRoleFail', () => { + it('changeOrganizationMemberRoleFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CHANGE_MEMBER_ROLE_ORGANIZATION_ERROR, + payload: {} + } + expect(actions.changeOrganizationMemberRoleFail()).toEqual(expectedResult) + }) + }) + describe('relRoleMember', () => { + it('relRoleMember should return the correct type', () => { + const expectedResult = { + type: ActionTypes.REL_ROLE_MEMBER, + payload: { + id: orgId, + memberIds: [1, 2], + resolve + } + } + expect(actions.relRoleMember(orgId, [1, 2], resolve)).toEqual( + expectedResult + ) + }) + }) + describe('relRoleMemberSuccess', () => { + it('relRoleMemberSuccess should return the correct type', () => { + const expectedResult = { + type: ActionTypes.REL_ROLE_MEMBER_SUCCESS, + payload: {} + } + expect(actions.relRoleMemberSuccess()).toEqual(expectedResult) + }) + }) + describe('relRoleMemberFail', () => { + it('relRoleMemberFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.REL_ROLE_MEMBER_FAILURE, + payload: {} + } + expect(actions.relRoleMemberFail()).toEqual(expectedResult) + }) + }) + describe('getRelRoleMember', () => { + it('getRelRoleMember should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_REL_ROLE_MEMBER, + payload: { + id: orgId, + resolve + } + } + expect(actions.getRelRoleMember(orgId, resolve)).toEqual(expectedResult) + }) + }) + describe('getRelRoleMemberSuccess', () => { + it('getRelRoleMemberSuccess should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_REL_ROLE_MEMBER_SUCCESS, + payload: {} + } + expect(actions.getRelRoleMemberSuccess()).toEqual(expectedResult) + }) + }) + describe('getRelRoleMemberFail', () => { + it('getRelRoleMemberFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_REL_ROLE_MEMBER_FAILURE, + payload: {} + } + expect(actions.getRelRoleMemberFail()).toEqual(expectedResult) + }) + }) + describe('setCurrentProject', () => { + it('setCurrentProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.SET_CURRENT_ORIGANIZATION_PROJECT, + payload: { + option: {} + } + } + expect(actions.setCurrentProject({})).toEqual(expectedResult) + }) + }) + describe('loadProjectAdmin', () => { + it('loadProjectAdmin should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_ADMINS, + payload: { + projectId: orgId + } + } + expect(actions.loadProjectAdmin(orgId)).toEqual(expectedResult) + }) + }) + describe('projectAdminLoaded', () => { + it('projectAdminLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_ADMINS_SUCCESS, + payload: { + result: projects + } + } + expect(actions.projectAdminLoaded(projects)).toEqual(expectedResult) + }) + }) + describe('loadProjectAdminFail', () => { + it('loadProjectAdminFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_ADMINS_FAIL, + payload: {} + } + expect(actions.loadProjectAdminFail()).toEqual(expectedResult) + }) + }) + describe('loadProjectRoles', () => { + it('loadProjectRoles should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_ROLES, + payload: { + projectId: orgId + } + } + expect(actions.loadProjectRoles(orgId)).toEqual(expectedResult) + }) + }) + describe('projectRolesLoaded', () => { + it('projectRolesLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_ROLES_SUCCESS, + payload: { + result: roles + } + } + expect(actions.projectRolesLoaded(roles)).toEqual(expectedResult) + }) + }) + describe('loadProjectRolesFail', () => { + it('loadProjectRolesFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_ROLES_FAIL, + payload: {} + } + expect(actions.loadProjectRolesFail()).toEqual(expectedResult) + }) + }) + describe('getVizVisbility', () => { + it('getVizVisbility should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_VIZ_VISBILITY, + payload: { + roleId: orgId, + projectId: orgId, + resolve + } + } + expect(actions.getVizVisbility(orgId, orgId, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('postVizVisbility', () => { + it('postVizVisbility should return the correct type', () => { + const expectedResult = { + type: ActionTypes.POST_VIZ_VISBILITY, + payload: { + id: orgId, + resolve, + permission: [] + } + } + expect(actions.postVizVisbility(orgId, [], resolve)).toEqual( + expectedResult + ) + }) + }) + describe('getRoleListByMemberId', () => { + it('getRoleListByMemberId should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_ROLELISTS_BY_MEMBERID, + payload: { + orgId, + memberId: orgId, + resolve + } + } + expect(actions.getRoleListByMemberId(orgId, orgId, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('getRoleListByMemberIdSuccess', () => { + it('getRoleListByMemberIdSuccess should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_ROLELISTS_BY_MEMBERID_SUCCESS, + payload: { + result: member, + memberId: orgId + } + } + expect(actions.getRoleListByMemberIdSuccess(member, orgId)).toEqual( + expectedResult + ) + }) + }) + describe('getRoleListByMemberIdFail', () => { + it('getRoleListByMemberIdFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_ROLELISTS_BY_MEMBERID_ERROR, + payload: { + error: 'error', + memberId: orgId + } + } + expect(actions.getRoleListByMemberIdFail('error', orgId)).toEqual( + expectedResult + ) + }) + }) +}) diff --git a/webapp/test/app/containers/Organizations/fixtures.ts b/webapp/test/app/containers/Organizations/fixtures.ts new file mode 100644 index 000000000..babff992a --- /dev/null +++ b/webapp/test/app/containers/Organizations/fixtures.ts @@ -0,0 +1,82 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { IMembers, IOrganizationRole, IOrganization } from 'app/containers/Organizations/types' +import { IProject } from 'app/containers/Projects/types' +import { ProjectDemo } from '../Projects/fixtures' + +const memberDemo: IMembers = { + id: 811, + user: { + avatar: '', + email: '', + id: 1, + role: 0, + username: 'shan' + } +} + +const orgDemo: IOrganization = { + id: 1, + name: 'string', + avatar: '', + description: '' +} + +const roleDemo: IOrganizationRole = { + id: 1, + name: 'roleName', + description: 'desc' +} + +interface ImockStore { + orgId: number + memberId: number + project: IProject + projects: IProject[] + orgProjects: { + list: IProject[] + } + member: IMembers + members: IMembers[] + role: IOrganizationRole + roles: IOrganizationRole[] + organization: IOrganization + organizations: IOrganization[] + resolve: () => void + +} + +export const mockStore: ImockStore = { + orgId: 1, + memberId: 1, + project: ProjectDemo, + projects: [ProjectDemo], + member: memberDemo, + members: [memberDemo], + role: roleDemo, + roles: [roleDemo], + organization: orgDemo, + organizations: [orgDemo], + orgProjects: { + list: [ProjectDemo] + }, + resolve: () => void 0 +} diff --git a/webapp/test/app/containers/Organizations/reducer.test.ts b/webapp/test/app/containers/Organizations/reducer.test.ts new file mode 100644 index 000000000..82dbb497d --- /dev/null +++ b/webapp/test/app/containers/Organizations/reducer.test.ts @@ -0,0 +1,252 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import produce from 'immer' +import reducer, { initialState } from 'app/containers/Organizations/reducer' +import actions from 'app/containers/Organizations/actions' +import { mockAnonymousAction } from 'test/utils/fixtures' +import { mockStore } from './fixtures' + +describe('organizationReducer', () => { + const { + orgId, + projects, + orgProjects, + organization, + organizations, + role, + members, + roles, + memberId + } = mockStore + let state + beforeEach(() => { + state = initialState + }) + + it('should return the initial state', () => { + expect(reducer(void 0, mockAnonymousAction)).toEqual(state) + }) + + it('should handle the organizationMemberDeleted action correctly', () => { + const expectedResult = produce(state, (draft) => { + if (draft.currentOrganizationMembers) { + draft.currentOrganizationMembers = draft.currentOrganizationMembers.filter( + (d) => d.id !== orgId + ) + } + }) + expect(reducer(state, actions.organizationMemberDeleted(orgId))).toEqual( + expectedResult + ) + }) + + it('should handle the organizationsProjectsLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentOrganizationProjects = orgProjects.list + draft.currentOrganizationProjectsDetail = orgProjects + }) + expect( + reducer(state, actions.organizationsProjectsLoaded(orgProjects)) + ).toEqual(expectedResult) + }) + + it('should handle the organizationsMembersLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentOrganizationMembers = members.map((member) => { + return { + ...member, + roles: 'loading' + } + }) + }) + expect(reducer(state, actions.organizationsMembersLoaded(members))).toEqual( + expectedResult + ) + }) + + it('should handle the getRoleListByMemberIdFail action correctly', () => { + const expectedResult = produce(state, (draft) => { + const mId = memberId + if (draft.currentOrganizationMembers) { + draft.currentOrganizationMembers = draft.currentOrganizationMembers.map( + (member) => + member.user.id === mId ? { ...member, roles: undefined } : member + ) + } + }) + expect( + reducer(state, actions.getRoleListByMemberIdFail('error', memberId)) + ).toEqual(expectedResult) + }) + + it('should handle the getRoleListByMemberIdSuccess action correctly', () => { + const expectedResult = produce(state, (draft) => { + if (draft.currentOrganizationMembers) { + draft.currentOrganizationMembers = draft.currentOrganizationMembers.map( + (member) => + member.user.id === memberId ? { ...member, roles } : member + ) + } + }) + expect( + reducer(state, actions.getRoleListByMemberIdSuccess(roles, memberId)) + ).toEqual(expectedResult) + }) + + it('should handle the organizationsRoleLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentOrganizationRole = role + }) + expect(reducer(state, actions.organizationsRoleLoaded(role))).toEqual( + expectedResult + ) + }) + + it('should handle the organizationsLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.organizations = organizations + }) + expect(reducer(state, actions.organizationsLoaded(organizations))).toEqual( + expectedResult + ) + }) + + it('should handle the organizationAdded action correctly', () => { + const expectedResult = produce(state, (draft) => { + if (draft.organizations) { + draft.organizations.unshift(organization) + } else { + draft.organizations = [organization] + } + }) + expect(reducer(state, actions.organizationAdded(organization))).toEqual( + expectedResult + ) + }) + + it('should handle the organizationEdited action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.organizations.splice( + draft.organizations.findIndex((d) => d.id === organization.id), + 1, + organization + ) + }) + expect(reducer(state, actions.organizationEdited(organization))).toEqual( + expectedResult + ) + }) + + it('should handle the organizationDeleted action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.organizations = draft.organizations.filter((d) => d.id !== orgId) + }) + expect(reducer(state, actions.organizationDeleted(orgId))).toEqual( + expectedResult + ) + }) + + it('should handle the loadOrganizationDetail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentOrganizationLoading = true + }) + expect(reducer(state, actions.loadOrganizationDetail(orgId))).toEqual( + expectedResult + ) + }) + + it('should handle the organizationDetailLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentOrganizationLoading = false + draft.currentOrganization = organization + }) + expect( + reducer(state, actions.organizationDetailLoaded(organization)) + ).toEqual(expectedResult) + }) + + it('should handle the roleAdded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.roleModalLoading = false + }) + expect(reducer(state, actions.roleAdded(role))).toEqual(expectedResult) + }) + + it('should handle the addRoleFail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.roleModalLoading = false + }) + expect(reducer(state, actions.addRoleFail())).toEqual(expectedResult) + }) + + it('should handle the searchMember action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.inviteMemberfetching = true + }) + expect(reducer(state, actions.searchMember('keyword'))).toEqual( + expectedResult + ) + }) + + it('should handle the memberSearched action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.inviteMemberLists = members + draft.inviteMemberfetching = false + }) + expect(reducer(state, actions.memberSearched(members))).toEqual( + expectedResult + ) + }) + + it('should handle the searchMemberFail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.inviteMemberfetching = true + }) + expect(reducer(state, actions.searchMemberFail())).toEqual(expectedResult) + }) + + it('should handle the setCurrentProject action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.projectDetail = orgProjects + }) + expect(reducer(state, actions.setCurrentProject(orgProjects))).toEqual( + expectedResult + ) + }) + + it('should handle the projectAdminLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.projectAdmins = projects + }) + expect(reducer(state, actions.projectAdminLoaded(projects))).toEqual( + expectedResult + ) + }) + + it('should handle the projectRolesLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.projectRoles = role + }) + expect(reducer(state, actions.projectRolesLoaded(role))).toEqual( + expectedResult + ) + }) +}) diff --git a/webapp/test/app/containers/Organizations/saga.test.ts b/webapp/test/app/containers/Organizations/saga.test.ts new file mode 100644 index 000000000..3ef879503 --- /dev/null +++ b/webapp/test/app/containers/Organizations/saga.test.ts @@ -0,0 +1,444 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { throwError } from 'redux-saga-test-plan/providers' +import request from 'app/utils/request' +import actions from 'app/containers/Organizations/actions' +import { + getOrganizations, + addOrganization, + editOrganization, + deleteOrganization, + getOrganizationDetail, + getOrganizationsProjects, + getOrganizationsMembers, + getOrganizationsRole, + addRole, + getRoleListByMemberId, + deleteRole, + editRole, + relRoleMember, + getRelRoleMember, + searchMember, + inviteMember, + deleteOrganizationMember, + changeOrganizationMemberRole, + getProjectAdmins, + getVizVisbility, + postVizVisbility +} from 'app/containers/Organizations/sagas' +import { mockStore } from './fixtures' +import { getMockResponse } from 'test/utils/fixtures' + +describe('Organizations Sagas', () => { + const { projects, organization, orgId, organizations, roles, role, member, members } = mockStore + describe('getOrganizations Saga', () => { + it('should dispatch the organizationsLoaded action if it requests the data successfully', () => { + return expectSaga(getOrganizations) + .provide([[matchers.call.fn(request), getMockResponse(projects)]]) + .put(actions.organizationsLoaded(projects)) + .run() + }) + + it('should call the loadOrganizationsFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getOrganizations) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadOrganizationsFail()) + .run() + }) + }) + + describe('addOrganization Saga', () => { + const addOrganizationActions = actions.addOrganization(organization, () => void 0) + it('should dispatch the organizationAdded action if it requests the data successfully', () => { + return expectSaga(addOrganization, addOrganizationActions) + .provide([[matchers.call.fn(request), getMockResponse(projects)]]) + .put(actions.organizationAdded(projects)) + .run() + }) + + it('should call the addOrganizationFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(addOrganization, addOrganizationActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.addOrganizationFail()) + .run() + }) + + }) + + describe('editOrganization Saga', () => { + const editOrganizationActions = actions.editOrganization(organization) + it('should dispatch the organizationEdited action if it requests the data successfully', () => { + return expectSaga(editOrganization, editOrganizationActions) + .provide([[matchers.call.fn(request), getMockResponse(organization)]]) + .put(actions.organizationEdited(organization)) + .run() + }) + + it('should call the editOrganizationFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(editOrganization, editOrganizationActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.editOrganizationFail()) + .run() + }) + + }) + + describe('deleteOrganization Saga', () => { + const deleteOrganizationActions = actions.deleteOrganization(orgId, () => void 0) + it('should dispatch the organizationDeleted action if it requests the data successfully', () => { + return expectSaga(deleteOrganization, deleteOrganizationActions) + .provide([[matchers.call.fn(request), getMockResponse(organization)]]) + .put(actions.organizationDeleted(orgId)) + .run() + }) + + it('should call the deleteOrganizationFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(deleteOrganization, deleteOrganizationActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.deleteOrganizationFail()) + .run() + }) + + }) + + describe('getOrganizationDetail Saga', () => { + const loadOrganizationDetailActions = actions.loadOrganizationDetail(orgId) + it('should dispatch the organizationDeleted action if it requests the data successfully', () => { + return expectSaga(getOrganizationDetail, loadOrganizationDetailActions) + .provide([[matchers.call.fn(request), getMockResponse(organization)]]) + .put(actions.organizationDetailLoaded(orgId)) + .run() + }) + }) + + describe('getOrganizationsProjects Saga', () => { + const [id, keyword, pageNum, pageSize] = [orgId, 'password', 1, 20] + const loadOrganizationProjectsActions = actions.loadOrganizationProjects({id, keyword, pageNum, pageSize}) + it('should dispatch the organizationsProjectsLoaded action if it requests the data successfully', () => { + return expectSaga(getOrganizationsProjects, loadOrganizationProjectsActions) + .provide([[matchers.call.fn(request), getMockResponse(organizations)]]) + .put(actions.organizationsProjectsLoaded(organizations)) + .run() + }) + + it('should call the loadOrganizationsProjectsFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getOrganizationsProjects, loadOrganizationProjectsActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadOrganizationsProjectsFail()) + .run() + }) + + }) + + describe('getOrganizationsMembers Saga', () => { + const loadOrganizationMembersActions = actions.loadOrganizationMembers(orgId) + it('should dispatch the organizationsMembersLoaded action if it requests the data successfully', () => { + return expectSaga(getOrganizationsMembers, loadOrganizationMembersActions) + .provide([[matchers.call.fn(request), getMockResponse(organizations)]]) + .put(actions.organizationsMembersLoaded(organizations)) + .run() + }) + + it('should call the loadOrganizationsMembersFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getOrganizationsMembers, loadOrganizationMembersActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadOrganizationsMembersFail()) + .run() + }) + + }) + + describe('getOrganizationsMembers Saga', () => { + const loadOrganizationRoleActions = actions.loadOrganizationRole(orgId) + it('should dispatch the organizationsRoleLoaded action if it requests the data successfully', () => { + return expectSaga(getOrganizationsRole, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), getMockResponse(organizations)]]) + .put(actions.organizationsRoleLoaded(organizations)) + .run() + }) + + it('should call the loadOrganizationsRoleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getOrganizationsRole, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadOrganizationsRoleFail()) + .run() + }) + + }) + + describe('addRole Saga', () => { + const [name, description, id, resolve] = ['name', 'desc', orgId, () => void 0] + const loadOrganizationRoleActions = actions.addRole(name, description, id, resolve) + it('should dispatch the roleAdded action if it requests the data successfully', () => { + return expectSaga(addRole, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), getMockResponse(roles)]]) + .put(actions.roleAdded(roles)) + .run() + }) + + it('should call the addRoleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(addRole, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.addRoleFail()) + .run() + }) + + }) + + describe('getRoleListByMemberId Saga', () => { + const [memberId, orgId, resolve] = [1, 1, () => void 0] + const loadOrganizationRoleActions = actions.getRoleListByMemberId(memberId, orgId, resolve) + it('should dispatch the getRoleListByMemberIdSuccess action if it requests the data successfully', () => { + return expectSaga(getRoleListByMemberId, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), getMockResponse(roles)]]) + .put(actions.getRoleListByMemberIdSuccess(roles, memberId)) + .run() + }) + + it('should call the getRoleListByMemberIdFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getRoleListByMemberId, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.getRoleListByMemberIdFail(errors, memberId)) + .run() + }) + + }) + + describe('deleteRole Saga', () => { + const [id, resolve ] = [1, () => void 0] + const loadOrganizationRoleActions = actions.deleteRole(id, resolve) + it('should dispatch the roleDeleted action if it requests the data successfully', () => { + return expectSaga(deleteRole, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), getMockResponse(role)]]) + .put(actions.roleDeleted(role)) + .run() + }) + + it('should call the deleteRoleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(deleteRole, loadOrganizationRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.deleteRoleFail()) + .run() + }) + + }) + + describe('editRole Saga', () => { + const [name, description, id, resolve] = ['name', 'desc', 1, () => void 0] + const editRoleActions = actions.editRole(name, description, id, resolve) + it('should dispatch the roleEdited action if it requests the data successfully', () => { + return expectSaga(editRole, editRoleActions) + .provide([[matchers.call.fn(request), getMockResponse(role)]]) + .put(actions.roleEdited(role)) + .run() + }) + + it('should call the editRoleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(editRole, editRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.editRoleFail()) + .run() + }) + + }) + + + + describe('relRoleMember Saga', () => { + const [id, memberIds, resolve] = [ 1, [1], () => void 0] + const relRoleMemberActions = actions.relRoleMember(id, memberIds, resolve) + it('should dispatch the relRoleMemberSuccess action if it requests the data successfully', () => { + return expectSaga(relRoleMember, relRoleMemberActions) + .provide([[matchers.call.fn(request), getMockResponse(role)]]) + .put(actions.relRoleMemberSuccess()) + .run() + }) + + it('should call the relRoleMemberFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(relRoleMember, relRoleMemberActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.relRoleMemberFail()) + .run() + }) + + }) + + + describe('relRoleMember Saga', () => { + const [id, resolve] = [ 1, () => void 0] + const getRelRoleMemberActions = actions.getRelRoleMember(id, resolve) + it('should dispatch the getRelRoleMemberSuccess action if it requests the data successfully', () => { + return expectSaga(getRelRoleMember, getRelRoleMemberActions) + .provide([[matchers.call.fn(request), getMockResponse(role)]]) + .put(actions.getRelRoleMemberSuccess()) + .run() + }) + + it('should call the getRelRoleMemberFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getRelRoleMember, getRelRoleMemberActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.getRelRoleMemberFail()) + .run() + }) + + }) + + + + describe('searchMember Saga', () => { + const searchMemberActions = actions.searchMember('keywords') + it('should dispatch the memberSearched action if it requests the data successfully', () => { + return expectSaga(searchMember, searchMemberActions) + .provide([[matchers.call.fn(request), getMockResponse(member)]]) + .put(actions.memberSearched(member)) + .run() + }) + + it('should call the searchMemberFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(searchMember, searchMemberActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.searchMemberFail()) + .run() + }) + + }) + + + describe('inviteMember Saga', () => { + const [ orgId, members, needEmail, resolve] = [1, [member], false, () => void 0 ] + const inventMemberActions = actions.inviteMember(orgId, members, needEmail, resolve) + it('should dispatch the inviteMemberSuccess action if it requests the data successfully', () => { + return expectSaga(inviteMember, inventMemberActions) + .provide([[matchers.call.fn(request), getMockResponse(member)]]) + .put(actions.inviteMemberSuccess(member)) + .run() + }) + + it('should call the inviteMemberFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(inviteMember, inventMemberActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.inviteMemberFail()) + .run() + }) + + }) + + + describe('deleteOrganizationMember Saga', () => { + const [ relationId, resolve] = [1, () => void 0 ] + const deleteOrganizationMemberActions = actions.deleteOrganizationMember(relationId, resolve) + it('should dispatch the organizationMemberDeleted action if it requests the data successfully', () => { + return expectSaga(deleteOrganizationMember, deleteOrganizationMemberActions) + .provide([[matchers.call.fn(request), getMockResponse(relationId)]]) + .put(actions.organizationMemberDeleted(relationId)) + .run() + }) + + it('should call the deleteOrganizationMemberFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(deleteOrganizationMember, deleteOrganizationMemberActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.deleteOrganizationMemberFail()) + .run() + }) + + }) + + describe('changeOrganizationMemberRole Saga', () => { + const [ relationId, newRole, resolve ] = [1, role, () => void 0 ] + const changeOrganizationMemberRoleActions = actions.changeOrganizationMemberRole(relationId, newRole, resolve) + it('should dispatch the organizationMemberRoleChanged action if it requests the data successfully', () => { + return expectSaga(changeOrganizationMemberRole, changeOrganizationMemberRoleActions) + .provide([[matchers.call.fn(request), getMockResponse(member)]]) + .put(actions.organizationMemberRoleChanged(relationId, member)) + .run() + }) + + it('should call the changeOrganizationMemberRoleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(changeOrganizationMemberRole, changeOrganizationMemberRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.changeOrganizationMemberRoleFail()) + .run() + }) + + }) + + describe('getProjectAdmins Saga', () => { + const changeOrganizationMemberRoleActions = actions.loadProjectAdmin(orgId) + it('should dispatch the projectAdminLoaded action if it requests the data successfully', () => { + return expectSaga(getProjectAdmins, changeOrganizationMemberRoleActions) + .provide([[matchers.call.fn(request), getMockResponse(members)]]) + .put(actions.projectAdminLoaded(members)) + .run() + }) + + it('should call the loadProjectAdminFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getProjectAdmins, changeOrganizationMemberRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadProjectAdminFail()) + .run() + }) + + }) + + + describe('getVizVisbility Saga', () => { + const [roleId, projectId, resolve] = [ orgId, orgId, () => void 0] + const getVizVisbilityActions = actions.getVizVisbility(roleId, projectId, resolve) + it('should dispatch the getVizVisbilitySaga action if it requests the data successfully', () => { + return expectSaga(getVizVisbility, getVizVisbilityActions) + .provide([[matchers.call.fn(request), getMockResponse({})]]) + .run() + }) + + }) + + + describe('postVizVisbility Saga', () => { + const [id, permission, resolve] = [ orgId, {}, () => void 0] + const postVizVisbilityActions = actions.postVizVisbility(id, permission, resolve) + it('should dispatch the postVizVisbilityActions action if it requests the data successfully', () => { + return expectSaga(getVizVisbility, postVizVisbilityActions) + .provide([[matchers.call.fn(request), getMockResponse({})]]) + .run() + }) + + }) +}) diff --git a/webapp/test/app/containers/Organizations/selectors.test.ts b/webapp/test/app/containers/Organizations/selectors.test.ts new file mode 100644 index 000000000..586884ba5 --- /dev/null +++ b/webapp/test/app/containers/Organizations/selectors.test.ts @@ -0,0 +1,131 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { + selectOrganization, + makeSelectOrganizations, + makeSelectCurrentOrganizations, + makeSelectCurrentOrganizationProjects, + makeSelectCurrentOrganizationProjectsDetail, + makeSelectCurrentOrganizationRole, + makeSelectCurrentOrganizationMembers, + makeSelectInviteMemberList, + makeSelectRoleModalLoading, + makeSelectCurrentOrganizationProject, + makeSelectCurrentOrganizationProjectAdmins, + makeSelectCurrentOrganizationProjectRoles, + makeSelectInviteMemberLoading +} from 'app/containers/Organizations/selectors' +import { initialState } from 'app/containers/Organizations/reducer' + +const state = { + organization: initialState +} + +describe('selectProject', () => { + it('should select the org state', () => { + expect(selectOrganization(state)).toEqual(state.organization) + }) +}) + +describe('makeSelectProjects', () => { + const orgSelector = makeSelectOrganizations() + const currentOrgSelector = makeSelectCurrentOrganizations() + const currentOrgProsSelector = makeSelectCurrentOrganizationProjects() + const currentOrgProDetailSelector = makeSelectCurrentOrganizationProjectsDetail() + const currentOrgRoleSelector = makeSelectCurrentOrganizationRole() + const currentOrgMemberSelector = makeSelectCurrentOrganizationMembers() + const inviteMemberSelector = makeSelectInviteMemberList() + const modalLoadingSelector = makeSelectRoleModalLoading() + const currentOrgProSelector = makeSelectCurrentOrganizationProject() + const currentOrgProAdminSelector = makeSelectCurrentOrganizationProjectAdmins() + const currentOrgProRolesSelector = makeSelectCurrentOrganizationProjectRoles() + const inviteMemberLoadingSelector = makeSelectInviteMemberLoading() + + it('should select the orgSelector', () => { + expect(orgSelector(state)).toEqual(state.organization.organizations) + }) + + it('should select the currentOrgSelector', () => { + expect(currentOrgSelector(state)).toEqual( + state.organization.currentOrganization + ) + }) + + it('should select the currentOrgProsSelector', () => { + expect(currentOrgProsSelector(state)).toEqual( + state.organization.organizations + ) + }) + + it('should select the currentOrgProDetailSelector', () => { + expect(currentOrgProDetailSelector(state)).toEqual( + state.organization.currentOrganizationProjectsDetail + ) + }) + + it('should select the currentOrgRoleSelector', () => { + expect(currentOrgRoleSelector(state)).toEqual( + state.organization.currentOrganizationRole + ) + }) + + it('should select the currentOrgMemberSelector', () => { + expect(currentOrgMemberSelector(state)).toEqual( + state.organization.currentOrganizationMembers + ) + }) + + it('should select the inviteMemberSelector', () => { + expect(inviteMemberSelector(state)).toEqual( + state.organization.inviteMemberLists + ) + }) + + it('should select the modalLoadingSelector', () => { + expect(modalLoadingSelector(state)).toEqual( + state.organization.roleModalLoading + ) + }) + + it('should select the currentOrgProSelector', () => { + expect(currentOrgProSelector(state)).toEqual( + state.organization.projectDetail + ) + }) + + it('should select the currentOrgProAdminSelector', () => { + expect(currentOrgProAdminSelector(state)).toEqual( + state.organization.projectAdmins + ) + }) + + it('should select the currentOrgProRolesSelector', () => { + expect(currentOrgProRolesSelector(state)).toEqual( + state.organization.projectRoles + ) + }) + + it('should select the inviteMemberLoadingSelector', () => { + expect(inviteMemberLoadingSelector(state)).toEqual( + state.organization.inviteMemberfetching + ) + }) +}) diff --git a/webapp/test/app/containers/Profile/actions.test.ts b/webapp/test/app/containers/Profile/actions.test.ts new file mode 100644 index 000000000..718de4dd4 --- /dev/null +++ b/webapp/test/app/containers/Profile/actions.test.ts @@ -0,0 +1,61 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { + GET_USER_PROFILE, + GET_USER_PROFILE_FAILURE, + GET_USER_PROFILE_SUCCESS +} from 'app/containers/Profile/constants' +import actions from 'app/containers/Profile/actions' +import { mockStore } from './fixtures' + +describe('Profile Actions', () => { + const { userId, profile } = mockStore + describe('getUserProfile', () => { + it('getUserProfile should return the correct type', () => { + const expectedResult = { + type: GET_USER_PROFILE, + payload: { + id: userId + } + } + expect(actions.getUserProfile(userId)).toEqual(expectedResult) + }) + }) + describe('userProfileGot', () => { + it('userProfileGot should return the correct type', () => { + const expectedResult = { + type: GET_USER_PROFILE_SUCCESS, + payload: { + result: profile + } + } + expect(actions.userProfileGot(profile)).toEqual(expectedResult) + }) + }) + describe('getUserProfileFail', () => { + it('getUserProfileFail should return the correct type', () => { + const expectedResult = { + type: GET_USER_PROFILE_FAILURE + } + expect(actions.getUserProfileFail()).toEqual(expectedResult) + }) + }) +}) diff --git a/webapp/test/app/containers/Profile/fixtures.ts b/webapp/test/app/containers/Profile/fixtures.ts new file mode 100644 index 000000000..c5ea90a9c --- /dev/null +++ b/webapp/test/app/containers/Profile/fixtures.ts @@ -0,0 +1,29 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +interface ImockStore { + userId: number + profile: string +} + +export const mockStore: ImockStore = { + userId: 1, + profile: 'profile' +} diff --git a/webapp/test/app/containers/Profile/reducer.test.ts b/webapp/test/app/containers/Profile/reducer.test.ts new file mode 100644 index 000000000..b7deea694 --- /dev/null +++ b/webapp/test/app/containers/Profile/reducer.test.ts @@ -0,0 +1,56 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import produce from 'immer' +import reducer, { initialState } from 'app/containers/Profile/reducer' +import actions from 'app/containers/Profile/actions' +import { mockAnonymousAction } from 'test/utils/fixtures' +import { mockStore } from './fixtures' + +describe('projectsReducer', () => { + const { profile, userId } = mockStore + let state + beforeEach(() => { + state = initialState + }) + + it('should return the initial state', () => { + expect(reducer(void 0, mockAnonymousAction)).toEqual(state) + }) + + it('should handle the getUserProfile action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading = true + }) + expect(reducer(state, actions.getUserProfile(userId))).toEqual( + expectedResult + ) + }) + + it('should handle the relRoleProjectLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading = false + draft.userProfile = profile + }) + expect(reducer(state, actions.userProfileGot(profile))).toEqual( + expectedResult + ) + }) +}) diff --git a/webapp/test/app/containers/Profile/sagas.test.ts b/webapp/test/app/containers/Profile/sagas.test.ts new file mode 100644 index 000000000..c38940c4a --- /dev/null +++ b/webapp/test/app/containers/Profile/sagas.test.ts @@ -0,0 +1,52 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { throwError } from 'redux-saga-test-plan/providers' +import request from 'app/utils/request' +import actions from 'app/containers/Profile/actions' +import { + getUserProfile +} from 'app/containers/Profile/sagas' +import { mockStore } from './fixtures' +import { getMockResponse } from 'test/utils/fixtures' + +describe('Profile Sagas', () => { + const { userId, profile } = mockStore + describe('getUserProfile Saga', () => { + const getUserProfileActions = actions.getUserProfile(userId) + it('should dispatch the userProfileGot action if it requests the data successfully', () => { + return expectSaga(getUserProfile, getUserProfileActions) + .provide([[matchers.call.fn(request), getMockResponse(profile)]]) + .put(actions.userProfileGot(profile)) + .run() + }) + + it('should call the getUserProfileFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getUserProfile, getUserProfileActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.getUserProfileFail()) + .run() + }) + }) + +}) diff --git a/webapp/test/app/containers/Profile/selectors.test.ts b/webapp/test/app/containers/Profile/selectors.test.ts new file mode 100644 index 000000000..72f10edb9 --- /dev/null +++ b/webapp/test/app/containers/Profile/selectors.test.ts @@ -0,0 +1,50 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { + selectProfile, + makeSelectLoading, + makeSelectUserProfile +} from 'app/containers/Profile/selectors' +import { initialState } from 'app/containers/Profile/reducer' + +const state = { + profile: initialState +} + +describe('selectProfile', () => { + it('should select the selectProfile state', () => { + expect(selectProfile(state)).toEqual(state.profile) + }) +}) + +describe('makeSelectProjects', () => { + const loadingSelector = makeSelectLoading() + const userProfileSelector = makeSelectUserProfile() + + + it('should select the loadingSelector', () => { + expect(loadingSelector(state)).toEqual(state.profile.loading) + }) + + it('should select the userProfileSelector', () => { + expect(userProfileSelector(state)).toEqual(state.profile.userProfile) + }) +}) diff --git a/webapp/test/app/containers/Projects/actions.test.ts b/webapp/test/app/containers/Projects/actions.test.ts new file mode 100644 index 000000000..653ec7973 --- /dev/null +++ b/webapp/test/app/containers/Projects/actions.test.ts @@ -0,0 +1,637 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ +import { ActionTypes } from 'app/containers/Projects/constants' +import actions from 'app/containers/Projects/actions' +import { mockStore } from './fixtures' + +describe('Projects Actions', () => { + const { + project, + projectId, + projects, + resolve, + orgId, + isFavorite, + adminIds, + relationId + } = mockStore + describe('clearCurrentProject', () => { + it('clearCurrentProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CLEAR_CURRENT_PROJECT + } + expect(actions.clearCurrentProject()).toEqual(expectedResult) + }) + }) + + describe('loadProjects', () => { + it('loadProjects should return the correct type', () => { + const expectedResult = { type: ActionTypes.LOAD_PROJECTS } + expect(actions.loadProjects()).toEqual(expectedResult) + }) + }) + describe('projectsLoaded', () => { + it('projectsLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECTS_SUCCESS, + payload: { + projects + } + } + expect(actions.projectsLoaded(projects)).toEqual(expectedResult) + }) + }) + describe('loadProjectsFail', () => { + it('loadProjectsFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECTS_FAILURE + } + expect(actions.loadProjectsFail()).toEqual(expectedResult) + }) + }) + + describe('addProject', () => { + it('addProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT, + payload: { + project, + resolve + } + } + expect(actions.addProject(project, resolve)).toEqual(expectedResult) + }) + }) + describe('projectAdded', () => { + it('projectAdded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT_SUCCESS, + payload: { + result: project + } + } + expect(actions.projectAdded(project)).toEqual(expectedResult) + }) + }) + describe('addProjectFail', () => { + it('addProjectFail should return the correct type', () => { + const expectedResult = { type: ActionTypes.ADD_PROJECT_FAILURE } + expect(actions.addProjectFail()).toEqual(expectedResult) + }) + }) + + describe('editProject', () => { + it('editProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_PROJECT, + payload: { + project, + resolve + } + } + expect(actions.editProject(project, resolve)).toEqual(expectedResult) + }) + }) + describe('projectEdited', () => { + it('projectEdited should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_PROJECT_SUCCESS, + payload: { + result: project + } + } + expect(actions.projectEdited(project)).toEqual(expectedResult) + }) + }) + describe('editProjectFail', () => { + it('editProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EDIT_PROJECT_FAILURE + } + expect(actions.editProjectFail()).toEqual(expectedResult) + }) + }) + + describe('transferProject', () => { + it('transferProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.TRANSFER_PROJECT, + payload: { + id: projectId, + orgId, + resolve + } + } + expect(actions.transferProject(projectId, orgId, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('projectTransfered', () => { + it('projectTransfered should return the correct type', () => { + const expectedResult = { + type: ActionTypes.TRANSFER_PROJECT_SUCCESS, + payload: { result: project } + } + expect(actions.projectTransfered(project)).toEqual(expectedResult) + }) + }) + describe('transferProjectFail', () => { + it('transferProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.TRANSFER_PROJECT_FAILURE + } + expect(actions.transferProjectFail()).toEqual(expectedResult) + }) + }) + + describe('deleteProject', () => { + it('deleteProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT, + payload: { + id: projectId, + resolve + } + } + expect(actions.deleteProject(projectId, resolve)).toEqual(expectedResult) + }) + }) + describe('projectDeleted', () => { + it('projectDeleted should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_SUCCESS, + payload: { + id: projectId + } + } + expect(actions.projectDeleted(projectId)).toEqual(expectedResult) + }) + }) + describe('deleteProjectFail', () => { + it('deleteProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_FAILURE + } + expect(actions.deleteProjectFail()).toEqual(expectedResult) + }) + }) + + describe('loadProjectDetail', () => { + it('loadProjectDetail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_DETAIL, + payload: { id: projectId } + } + expect(actions.loadProjectDetail(projectId)).toEqual(expectedResult) + }) + }) + describe('projectDetailLoaded', () => { + it('projectDetailLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_DETAIL_SUCCESS, + payload: { + project + } + } + expect(actions.projectDetailLoaded(project)).toEqual(expectedResult) + }) + }) + describe('loadProjectDetailFail', () => { + it('loadProjectDetailFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PROJECT_DETAIL_FAILURE + } + expect(actions.loadProjectDetailFail()).toEqual(expectedResult) + }) + }) + + describe('searchProject', () => { + it('searchProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.SEARCH_PROJECT, + payload: { + param: 'projectName' + } + } + expect(actions.searchProject('projectName')).toEqual(expectedResult) + }) + }) + describe('projectSearched', () => { + it('projectSearched should return the correct type', () => { + const expectedResult = { + type: ActionTypes.SEARCH_PROJECT_SUCCESS, + payload: { + result: project + } + } + expect(actions.projectSearched(project)).toEqual(expectedResult) + }) + }) + describe('searchProjectFail', () => { + it('searchProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.SEARCH_PROJECT_FAILURE + } + expect(actions.searchProjectFail()).toEqual(expectedResult) + }) + }) + + describe('getProjectStarUser', () => { + it('getProjectStarUser should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_PROJECT_STAR_USER, + payload: { + id: projectId + } + } + expect(actions.getProjectStarUser(projectId)).toEqual(expectedResult) + }) + }) + describe('getProjectStarUserSuccess', () => { + it('getProjectStarUserSuccess should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_PROJECT_STAR_USER_SUCCESS, + payload: { result: project } + } + expect(actions.getProjectStarUserSuccess(project)).toEqual(expectedResult) + }) + }) + describe('getProjectStarUserFail', () => { + it('getProjectStarUserFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.GET_PROJECT_STAR_USER_FAILURE + } + expect(actions.getProjectStarUserFail()).toEqual(expectedResult) + }) + }) + + describe('unStarProject', () => { + it('unStarProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.PROJECT_UNSTAR, + payload: { + id: projectId, + resolve + } + } + expect(actions.unStarProject(projectId, resolve)).toEqual(expectedResult) + }) + }) + describe('unStarProjectSuccess', () => { + it('unStarProjectSuccess should return the correct type', () => { + const expectedResult = { + type: ActionTypes.PROJECT_UNSTAR_SUCCESS, + payload: { result: project } + } + expect(actions.unStarProjectSuccess(project)).toEqual(expectedResult) + }) + }) + describe('unStarProjectFail', () => { + it('unStarProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.PROJECT_UNSTAR_FAILURE + } + expect(actions.unStarProjectFail()).toEqual(expectedResult) + }) + }) + describe('loadCollectProjects', () => { + it('loadCollectProjects should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_COLLECT_PROJECTS + } + expect(actions.loadCollectProjects()).toEqual(expectedResult) + }) + }) + describe('collectProjectLoaded', () => { + it('collectProjectLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_COLLECT_PROJECTS_SUCCESS, + payload: { + result: project + } + } + expect(actions.collectProjectLoaded(project)).toEqual(expectedResult) + }) + }) + describe('collectProjectFail', () => { + it('collectProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_COLLECT_PROJECTS_FAILURE + } + expect(actions.collectProjectFail()).toEqual(expectedResult) + }) + }) + describe('clickCollectProjects', () => { + it('clickCollectProjects should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CLICK_COLLECT_PROJECT, + payload: { + isFavorite, + proId: projectId, + resolve + } + } + expect( + actions.clickCollectProjects(isFavorite, projectId, resolve) + ).toEqual(expectedResult) + }) + }) + describe('collectProjectClicked', () => { + it('collectProjectClicked should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CLICK_COLLECT_PROJECT_SUCCESS, + payload: { + result: project + } + } + expect(actions.collectProjectClicked(project)).toEqual(expectedResult) + }) + }) + describe('clickCollectProjectFail', () => { + it('clickCollectProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CLICK_COLLECT_PROJECT_FAILURE + } + expect(actions.clickCollectProjectFail()).toEqual(expectedResult) + }) + }) + describe('addProjectAdmin', () => { + it('addProjectAdmin should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT_ADMIN, + payload: { + id: projectId, + adminIds, + resolve + } + } + expect(actions.addProjectAdmin(projectId, adminIds, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('projectAdminAdded', () => { + it('projectAdminAdded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT_ADMIN_SUCCESS, + payload: { result: project } + } + expect(actions.projectAdminAdded(project)).toEqual(expectedResult) + }) + }) + describe('addProjectAdminFail', () => { + it('addProjectAdminFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT_ADMIN_FAIL + } + expect(actions.addProjectAdminFail()).toEqual(expectedResult) + }) + }) + describe('deleteProjectAdmin', () => { + it('deleteProjectAdmin should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_ADMIN, + payload: { + id: projectId, + relationId, + resolve + } + } + expect( + actions.deleteProjectAdmin(projectId, relationId, resolve) + ).toEqual(expectedResult) + }) + }) + describe('projectAdminDeleted', () => { + it('projectAdminDeleted should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_ADMIN_SUCCESS, + payload: { result: project } + } + expect(actions.projectAdminDeleted(project)).toEqual(expectedResult) + }) + }) + describe('deleteProjectAdminFail', () => { + it('deleteProjectAdminFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_ADMIN_FAIL + } + expect(actions.deleteProjectAdminFail()).toEqual(expectedResult) + }) + }) + describe('addProjectRole', () => { + it('addProjectRole should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT_ROLE, + payload: { + projectId, + roleIds: adminIds, + resolve + } + } + expect(actions.addProjectRole(projectId, adminIds, resolve)).toEqual( + expectedResult + ) + }) + + }) + describe('projectRoleAdded', () => { + it('projectRoleAdded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT_ROLE_SUCCESS, + payload: { result: project } + } + expect(actions.projectRoleAdded(project)).toEqual(expectedResult) + }) + }) + describe('addProjectRoleFail', () => { + it('addProjectRoleFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.ADD_PROJECT_ROLE_FAIL + } + expect(actions.addProjectRoleFail()).toEqual(expectedResult) + }) + }) + describe('deleteProjectRole', () => { + it('deleteProjectRole should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_ROLE, + payload: { + id: projectId, + relationId, + resolve + } + } + expect(actions.deleteProjectRole(projectId, relationId, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('projectRoleDeleted', () => { + it('projectRoleDeleted should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_ROLE_SUCCESS, + payload: { result: project } + } + expect(actions.projectRoleDeleted(project)).toEqual(expectedResult) + }) + }) + describe('deleteProjectRoleFail', () => { + it('deleteProjectRoleFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_PROJECT_ROLE_FAIL + } + expect(actions.deleteProjectRoleFail()).toEqual(expectedResult) + }) + }) + describe('updateRelRoleProject', () => { + it('updateRelRoleProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.UPDATE_RELATION_ROLE_PROJECT, + payload: { + roleId: relationId, + projectId, + projectRole: adminIds + } + } + expect( + actions.updateRelRoleProject(relationId, projectId, adminIds) + ).toEqual(expectedResult) + }) + }) + describe('relRoleProjectUpdated', () => { + it('relRoleProjectUpdated should return the correct type', () => { + const expectedResult = { + type: ActionTypes.UPDATE_RELATION_ROLE_PROJECT_SUCCESS, + payload: { result: project } + } + expect(actions.relRoleProjectUpdated(project)).toEqual(expectedResult) + }) + }) + describe('updateRelRoleProjectFail', () => { + it('updateRelRoleProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.UPDATE_RELATION_ROLE_PROJECT_FAIL + } + expect(actions.updateRelRoleProjectFail()).toEqual(expectedResult) + }) + }) + describe('deleteRelRoleProject', () => { + it('deleteRelRoleProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_RELATION_ROLE_PROJECT, + payload: { + roleId: relationId, + projectId, + resolve + } + } + expect( + actions.deleteRelRoleProject(relationId, projectId, resolve) + ).toEqual(expectedResult) + }) + }) + describe('relRoleProjectDeleted', () => { + it('relRoleProjectDeleted should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_RELATION_ROLE_PROJECT_SUCCESS, + payload: { result: project } + } + expect(actions.relRoleProjectDeleted(project)).toEqual(expectedResult) + }) + }) + describe('deleteRelRoleProjectFail', () => { + it('deleteRelRoleProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_RELATION_ROLE_PROJECT_FAIL + } + expect(actions.deleteRelRoleProjectFail()).toEqual(expectedResult) + }) + }) + describe('loadRelRoleProject', () => { + it('loadRelRoleProject should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_RELATION_ROLE_PROJECT, + payload: { + id: projectId, + roleId: relationId + } + } + expect(actions.loadRelRoleProject(projectId, relationId)).toEqual( + expectedResult + ) + }) + }) + describe('relRoleProjectLoaded', () => { + it('relRoleProjectLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.RELATION_ROLE_PROJECT_LOADED, + payload: { + result: project + } + } + expect(actions.relRoleProjectLoaded(project)).toEqual(expectedResult) + }) + }) + describe('loadRelRoleProjectFail', () => { + it('loadRelRoleProjectFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_RELATION_ROLE_PROJECT_FAIL + } + expect(actions.loadRelRoleProjectFail()).toEqual(expectedResult) + }) + }) + describe('excludeRoles', () => { + it('excludeRoles should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EXCLUDE_ROLES, + payload: { + type: 'roleType', + id: relationId, + resolve + } + } + expect(actions.excludeRoles('roleType', relationId, resolve)).toEqual( + expectedResult + ) + }) + }) + describe('rolesExcluded', () => { + it('rolesExcluded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EXCLUDE_ROLES_SUCCESS, + payload: { result: project } + } + expect(actions.rolesExcluded(project)).toEqual(expectedResult) + }) + }) + describe('excludeRolesFail', () => { + it('excludeRolesFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EXCLUDE_ROLES_FAIL, + payload: { + err: 'err' + } + } + expect(actions.excludeRolesFail('err')).toEqual(expectedResult) + }) + }) +}) diff --git a/webapp/test/app/containers/Projects/fixtures.ts b/webapp/test/app/containers/Projects/fixtures.ts new file mode 100644 index 000000000..0b9bc1f45 --- /dev/null +++ b/webapp/test/app/containers/Projects/fixtures.ts @@ -0,0 +1,90 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { + IProject, + IStarUser, + IProjectRole +} from 'app/containers/Projects/types' + +interface ImockStore { + projectId: number + project: IProject + projects: IProject[] + resolve: () => void + orgId: number + isFavorite: boolean + adminIds: number[] + relationId: number + user: IStarUser + role: IProjectRole +} + +export const ProjectDemo: IProject = { + createBy: { + avatar: '', + email: '', + id: 4, + username: '' + }, + description: '', + id: 4, + initialOrgId: 4, + isStar: true, + isTransfer: false, + name: 'test', + orgId: 4, + permission: { + downloadPermission: false, + schedulePermission: 0, + sharePermission: false, + sourcePermission: 0, + viewPermission: 0, + vizPermission: 1, + widgetPermission: 0 + }, + pic: '15', + starNum: 1, + userId: 4, + visibility: true +} + +export const mockStore: ImockStore = { + orgId: 1, + projectId: 1000, + projects: [ProjectDemo], + project: ProjectDemo, + resolve: () => void 0, + isFavorite: false, + adminIds: [1, 3], + relationId: 33, + user: { + avatar: '3c-f759-45c6-a3cb-a9b5ed88acfd.png', + email: 'xxxx@xxx.cn', + id: 5, + starTime: '2020-06-04 10:53:39', + username: 'xxxx' + }, + role: { + description: 'deving', + id: 7, + name: 'tank' + } +} diff --git a/webapp/test/app/containers/Projects/reducer.test.ts b/webapp/test/app/containers/Projects/reducer.test.ts new file mode 100644 index 000000000..589885f6f --- /dev/null +++ b/webapp/test/app/containers/Projects/reducer.test.ts @@ -0,0 +1,147 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import produce from 'immer' +import reducer, { initialState } from 'app/containers/Projects/reducer' +import actions from 'app/containers/Projects/actions' +import OrganizationActionTypes from 'app/containers/Organizations/actions' +import { mockAnonymousAction } from 'test/utils/fixtures' +import { mockStore } from './fixtures' + +describe('projectsReducer', () => { + const { projectId, projects, project, user, role } = mockStore + let state + beforeEach(() => { + state = initialState + }) + + it('should return the initial state', () => { + expect(reducer(void 0, mockAnonymousAction)).toEqual(state) + }) + + it('should handle the projectsLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.projects = projects + }) + expect(reducer(state, actions.projectsLoaded(projects))).toEqual( + expectedResult + ) + }) + + it('should handle the relRoleProjectLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentProjectRole = projects + }) + expect(reducer(state, actions.relRoleProjectLoaded(projects))).toEqual( + expectedResult + ) + }) + + it('should handle the projectAdded action correctly', () => { + const expectedResult = produce(state, (draft) => { + if (draft.projects) { + draft.projects.unshift(project) + } else { + draft.projects = [project] + } + }) + expect(reducer(state, actions.projectAdded(project))).toEqual( + expectedResult + ) + }) + + it('should handle the projectDeleted action correctly', () => { + const expectedResult = produce(state, (draft) => { + if (draft.projects) { + draft.projects = draft.projects.filter((d) => d.id !== project.id) + draft.collectProjects = draft.collectProjects.filter( + (d) => d.id !== project.id + ) + } + }) + expect(reducer(state, actions.projectDeleted(project.id))).toEqual( + expectedResult + ) + }) + + it('should handle the loadProjectDetail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentProjectLoading = true + }) + expect(reducer(state, actions.loadProjectDetail(project.id))).toEqual( + expectedResult + ) + }) + + it('should handle the clearCurrentProject action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentProject = null + }) + expect(reducer(state, actions.clearCurrentProject())).toEqual( + expectedResult + ) + }) + + it('should handle the projectDetailLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.currentProjectLoading = false + draft.currentProject = project + }) + expect(reducer(state, actions.projectDetailLoaded(project))).toEqual( + expectedResult + ) + }) + + it('should handle the projectSearched action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.searchProject = project + }) + expect(reducer(state, actions.projectSearched(project))).toEqual( + expectedResult + ) + }) + + it('should handle the getProjectStarUserSuccess action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.starUserList = [user] + }) + expect(reducer(state, actions.getProjectStarUserSuccess([user]))).toEqual( + expectedResult + ) + }) + + it('should handle the collectProjectLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.collectProjects = projects + }) + expect(reducer(state, actions.collectProjectLoaded(projects))).toEqual( + expectedResult + ) + }) + + it('should handle the projectRolesLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.projectRoles = [role] + }) + expect( + reducer(state, OrganizationActionTypes.projectRolesLoaded([role])) + ).toEqual(expectedResult) + }) +}) diff --git a/webapp/test/app/containers/Projects/sagas.test.ts b/webapp/test/app/containers/Projects/sagas.test.ts new file mode 100644 index 000000000..a5adf091f --- /dev/null +++ b/webapp/test/app/containers/Projects/sagas.test.ts @@ -0,0 +1,389 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { throwError } from 'redux-saga-test-plan/providers' +import request from 'app/utils/request' +import actions from 'app/containers/Projects/actions' +import OrgActions from 'app/containers/Organizations/actions' +import { + getProjects, + addProject, + editProject, + deleteProject, + addProjectAdmin, + addProjectRole, + transferProject, + searchProject, + unStarProject, + getProjectStarUser, + getCollectProjects, + editCollectProject, + loadRelRoleProject, + updateRelRoleProject, + deleteRelRoleProject, + getProjectRoles, + excludeRole +} from 'app/containers/Projects/sagas' +import { mockStore } from './fixtures' +import { getMockResponse } from 'test/utils/fixtures' + +describe('Projects Sagas', () => { + const { projects, project, projectId } = mockStore + describe('getProjects Saga', () => { + it('should dispatch the projectsLoaded action if it requests the data successfully', () => { + return expectSaga(getProjects, actions.loadProjects()) + .provide([[matchers.call.fn(request), getMockResponse(projects)]]) + .put(actions.projectsLoaded(projects)) + .run() + }) + + it('should call the loadProjectsFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getProjects, actions.loadProjects()) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadProjectsFail()) + .run() + }) + }) + + describe('addProjects Saga', () => { + const addProjectActions = actions.addProject(project, () => void 0) + it('should dispatch the projectAdded action if it requests the data successfully', () => { + return expectSaga(addProject, addProjectActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.projectAdded(project)) + .run() + }) + + it('should call the addProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(addProject, addProjectActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.addProjectFail()) + .run() + }) + }) + + describe('editProjects Saga', () => { + const editProjectActions = actions.editProject(project, () => void 0) + it('should dispatch the projectEdited action if it requests the data successfully', () => { + return expectSaga(editProject, editProjectActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.projectEdited(project)) + .run() + }) + + it('should call the editProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(editProject, editProjectActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.editProjectFail()) + .run() + }) + }) + + describe('deleteProjects Saga', () => { + const deleteProjectActions = actions.deleteProject(projectId, () => void 0) + it('should dispatch the projectDeleted action if it requests the data successfully', () => { + return expectSaga(deleteProject, deleteProjectActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.projectDeleted(projectId)) + .run() + }) + + it('should call the deleteProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(deleteProject, deleteProjectActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.deleteProjectFail()) + .run() + }) + }) + + describe('addProjectAdmin Saga', () => { + const addProjectAdminActions = actions.addProjectAdmin( + projectId, + projectId, + () => void 0 + ) + it('should call the addProjectAdminFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(addProjectAdmin, addProjectAdminActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.addProjectFail()) + .run() + }) + }) + + describe('addProjectRole Saga', () => { + const addProjectRoleActions = actions.addProjectRole( + projectId, + [projectId], + () => void 0 + ) + it('should call the addProjectRoleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(addProjectRole, addProjectRoleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.addProjectRoleFail()) + .run() + }) + }) + + describe('transferProject Saga', () => { + const transferProjectActions = actions.transferProject(projectId, projectId) + it('should dispatch the projectTransfered action if it requests the data successfully', () => { + return expectSaga(transferProject, transferProjectActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.projectTransfered(project)) + .run() + }) + + it('should call the transferProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(transferProject, transferProjectActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.transferProjectFail()) + .run() + }) + }) + + describe('searchProject Saga', () => { + const [keywords, pageNum, pageSize] = ['abc', 1, 20] + const transferProjectActions = actions.searchProject({keywords, pageNum, pageSize}) + it('should dispatch the projectSearched action if it requests the data successfully', () => { + return expectSaga(searchProject, transferProjectActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.projectSearched(project)) + .run() + }) + + it('should call the searchProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(searchProject, transferProjectActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.searchProjectFail()) + .run() + }) + }) + + + + describe('unStarProject Saga', () => { + const transferProjectActions = actions.unStarProject(projectId, () => void 0) + it('should dispatch the unStarProjectSuccess action if it requests the data successfully', () => { + return expectSaga(unStarProject, transferProjectActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.unStarProjectSuccess(project)) + .run() + }) + + it('should call the unStarProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(unStarProject, transferProjectActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.unStarProjectFail()) + .run() + }) + }) + + describe('getProjectStarUser Saga', () => { + const getProjectStarUserActions = actions.getProjectStarUser(projectId) + it('should dispatch the getProjectStarUserSuccess action if it requests the data successfully', () => { + return expectSaga(getProjectStarUser, getProjectStarUserActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.getProjectStarUserSuccess(project)) + .run() + }) + + it('should call the getProjectStarUserFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getProjectStarUser, getProjectStarUserActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.getProjectStarUserFail()) + .run() + }) + }) + + + describe('getCollectProjects Saga', () => { + it('should dispatch the collectProjectLoaded action if it requests the data successfully', () => { + return expectSaga(getCollectProjects) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.collectProjectLoaded(project)) + .run() + }) + + it('should call the collectProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getCollectProjects) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.collectProjectFail()) + .run() + }) + }) + + describe('editCollectProject Saga', () => { + const [ + isFavorite, + proId, + resolve] = [false, projectId, () => void 0] + const clickCollectProjectsActions = actions.clickCollectProjects(isFavorite, proId, resolve) + + it('should dispatch the collectProjectClicked action if it requests the data successfully', () => { + return expectSaga(editCollectProject, clickCollectProjectsActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.collectProjectClicked({isFavorite, proId, resolve})) + .run() + }) + + it('should call the clickCollectProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(editCollectProject, clickCollectProjectsActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.clickCollectProjectFail()) + .run() + }) + }) + + + describe('loadRelRoleProject Saga', () => { + const loadRelRoleProjectsActions = actions.loadRelRoleProject( + projectId, + projectId + ) + + it('should dispatch the relRoleProjectLoaded action if it requests the data successfully', () => { + return expectSaga(loadRelRoleProject, loadRelRoleProjectsActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.relRoleProjectLoaded(project)) + .run() + }) + + it('should call the loadRelRoleProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(loadRelRoleProject, loadRelRoleProjectsActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadRelRoleProjectFail()) + .run() + }) + }) + + + describe('updateRelRoleProject Saga', () => { + const loadRelRoleProjectsActions = actions.updateRelRoleProject( + projectId, + projectId, + project + ) + + it('should dispatch the relRoleProjectUpdated action if it requests the data successfully', () => { + return expectSaga(updateRelRoleProject, loadRelRoleProjectsActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.relRoleProjectUpdated(project)) + .run() + }) + + it('should call the updateRelRoleProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(updateRelRoleProject, loadRelRoleProjectsActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.updateRelRoleProjectFail()) + .run() + }) + + }) + + + describe('deleteRelRoleProject Saga', () => { + const deleteRelRoleProjectsActions = actions.deleteRelRoleProject( + projectId, + projectId, + () => void 0 + ) + + it('should dispatch the relRoleProjectDeleted action if it requests the data successfully', () => { + return expectSaga(deleteRelRoleProject, deleteRelRoleProjectsActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.relRoleProjectDeleted(project)) + .run() + }) + + it('should call the deleteRelRoleProjectFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(deleteRelRoleProject, deleteRelRoleProjectsActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.deleteRelRoleProjectFail()) + .run() + }) + + }) + + + describe('getProjectRoles Saga', () => { + const loadProjectRolesActions = OrgActions.loadProjectRoles( + projectId + ) + + it('should dispatch the projectRolesLoaded action if it requests the data successfully', () => { + return expectSaga(getProjectRoles, loadProjectRolesActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(OrgActions.projectRolesLoaded(project)) + .run() + }) + + it('should call the loadProjectRolesFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getProjectRoles, loadProjectRolesActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(OrgActions.loadProjectRolesFail()) + .run() + }) + + }) + + + describe('excludeRole Saga', () => { + const excludeRolesFailActions = actions.excludeRoles( + projectId, + 'type', + () => void 0 + ) + + it('should dispatch the rolesExcluded action if it requests the data successfully', () => { + return expectSaga(excludeRole, excludeRolesFailActions) + .provide([[matchers.call.fn(request), getMockResponse(project)]]) + .put(actions.rolesExcluded(project)) + .run() + }) + + it('should call the excludeRolesFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(excludeRole, excludeRolesFailActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.excludeRolesFail(errors)) + .run() + }) + + }) + +}) diff --git a/webapp/test/app/containers/Projects/selectors.test.ts b/webapp/test/app/containers/Projects/selectors.test.ts new file mode 100644 index 000000000..1c6500d1f --- /dev/null +++ b/webapp/test/app/containers/Projects/selectors.test.ts @@ -0,0 +1,84 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { + selectProject, + makeSelectProjects, + makeSelectCurrentProject, + makeSelectSearchProject, + makeSelectStarUserList, + makeSelectCollectProjects, + makeSelectCurrentProjectRole, + makeSelectProjectRoles +} from 'app/containers/Projects/selectors' +import { initialState } from 'app/containers/Projects/reducer' + +const state = { + projects: initialState +} + +describe('selectProject', () => { + it('should select the projects state', () => { + expect(selectProject(state)).toEqual(state.projects) + }) +}) + +describe('makeSelectProjects', () => { + const projectsSelector = makeSelectProjects() + + const currentProjectSelector = makeSelectCurrentProject() + const searchProjectsSelector = makeSelectSearchProject() + const starUserListSelector = makeSelectStarUserList() + const collectProjectsSelector = makeSelectCollectProjects() + const currentProjectsRoleSelector = makeSelectCurrentProjectRole() + const projectRolesSelector = makeSelectProjectRoles() + + it('should select the projects', () => { + expect(projectsSelector(state)).toEqual(state.projects.projects) + }) + + it('should select the currentProjectSelector', () => { + expect(currentProjectSelector(state)).toEqual(state.projects.currentProject) + }) + + it('should select the searchProjectsSelector', () => { + expect(searchProjectsSelector(state)).toEqual(state.projects.searchProject) + }) + + it('should select the starUserListSelector', () => { + expect(starUserListSelector(state)).toEqual(state.projects.starUserList) + }) + + it('should select the collectProjectsSelector', () => { + expect(collectProjectsSelector(state)).toEqual( + state.projects.collectProjects + ) + }) + + it('should select the currentProjectsRoleSelector', () => { + expect(currentProjectsRoleSelector(state)).toEqual( + state.projects.currentProjectRole + ) + }) + + it('should select the projectRolesSelector', () => { + expect(projectRolesSelector(state)).toEqual(state.projects.projectRoles) + }) +}) diff --git a/webapp/test/app/containers/Schedule/actions.test.ts b/webapp/test/app/containers/Schedule/actions.test.ts new file mode 100644 index 000000000..ba505c444 --- /dev/null +++ b/webapp/test/app/containers/Schedule/actions.test.ts @@ -0,0 +1,281 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { ActionTypes } from 'app/containers/Schedule/constants' +import actions from 'app/containers/Schedule/actions' +import {mockStore} from './fixtures' + +describe('Schedule Actions', () => { + const { schedule, projectId, mails, schedules, scheduleId, resolve, jobType } = mockStore + describe('loadSchedules', () => { + it('loadSchedules should return the correct type', () => { + const expectedResult = { type: ActionTypes.LOAD_SCHEDULES, payload: { + projectId + } } + expect(actions.loadSchedules(projectId)).toEqual(expectedResult) + }) + }) + describe('schedulesLoaded', () => { + it('schedulesLoaded should return the correct type', () => { + const expectedResult = { type: ActionTypes.LOAD_SCHEDULES_SUCCESS, payload: { + schedules + } } + expect(actions.schedulesLoaded(schedules)).toEqual(expectedResult) + }) + }) + describe('loadSchedulesFail', () => { + it('loadSchedulesFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_SCHEDULES_FAILURE, + payload: {} + } + expect(actions.loadSchedulesFail()).toEqual(expectedResult) + }) + }) + describe('loadScheduleDetail', () => { + it('loadScheduleDetail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_SCHEDULE_DETAIL, + payload: { + scheduleId + } + } + expect(actions.loadScheduleDetail(scheduleId)).toEqual(expectedResult) + }) + }) + describe('scheduleDetailLoaded', () => { + it('scheduleDetailLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_SCHEDULE_DETAIL_SUCCESS, + payload: { + schedule + } + } + expect(actions.scheduleDetailLoaded(schedule)).toEqual(expectedResult) + }) + }) + describe('loadScheduleDetailFail', () => { + it('loadScheduleDetailFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_SCHEDULE_DETAIL_FAILURE, + payload: {} + } + expect(actions.loadScheduleDetailFail()).toEqual(expectedResult) + }) + }) + describe('addSchedule', () => { + it('addSchedule should return the correct type', () => { + const expectedResult = { type: ActionTypes.ADD_SCHEDULE, payload: { + schedule, + resolve + } } + expect(actions.addSchedule(schedule, resolve)).toEqual(expectedResult) + }) + }) + describe('scheduleAdded', () => { + it('scheduleAdded should return the correct type', () => { + const expectedResult = { type: ActionTypes.ADD_SCHEDULE_SUCCESS, payload: { + result: schedule + } } + expect(actions.scheduleAdded(schedule)).toEqual(expectedResult) + }) + }) + describe('addScheduleFail', () => { + it('addScheduleFail should return the correct type', () => { + const expectedResult = { type: ActionTypes.ADD_SCHEDULE_FAILURE, payload: {} } + expect(actions.addScheduleFail()).toEqual(expectedResult) + }) + }) + describe('editSchedule', () => { + it('editSchedule should return the correct type', () => { + const expectedResult = { type: ActionTypes.EDIT_SCHEDULE, payload: { + schedule, + resolve + } } + expect(actions.editSchedule(schedule, + resolve)).toEqual(expectedResult) + }) + }) + describe('scheduleEdited', () => { + it('scheduleEdited should return the correct type', () => { + const expectedResult = { type: ActionTypes.EDIT_SCHEDULE_SUCCESS, payload: { + result: schedule + } } + expect(actions.scheduleEdited(schedule)).toEqual(expectedResult) + }) + }) + describe('editScheduleFail', () => { + it('editScheduleFail should return the correct type', () => { + const expectedResult = { type: ActionTypes.EDIT_SCHEDULE_FAILURE, payload: {} } + expect(actions.editScheduleFail()).toEqual(expectedResult) + }) + }) + describe('deleteSchedule', () => { + it('deleteSchedule should return the correct type', () => { + const expectedResult = { type: ActionTypes.DELETE_SCHEDULE, payload: { + id: scheduleId + } } + expect(actions.deleteSchedule(scheduleId)).toEqual(expectedResult) + }) + }) + describe('scheduleDeleted', () => { + it('scheduleDeleted should return the correct type', () => { + const expectedResult = { type: ActionTypes.DELETE_SCHEDULE, payload: { + id: scheduleId + } } + expect(actions.scheduleDeleted(scheduleId)).toEqual(expectedResult) + }) + }) + describe('deleteScheduleFail', () => { + it('deleteScheduleFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.DELETE_SCHEDULE_FAILURE, + payload: {} + } + expect(actions.deleteScheduleFail()).toEqual(expectedResult) + }) + }) + describe('changeSchedulesStatus', () => { + it('changeSchedulesStatus should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CHANGE_SCHEDULE_STATUS, + payload: { + id: scheduleId, + currentStatus: 'new' + } + } + expect(actions.changeSchedulesStatus(scheduleId, 'new')).toEqual(expectedResult) + }) + }) + describe('scheduleStatusChanged', () => { + it('scheduleStatusChanged should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CHANGE_SCHEDULE_STATUS_SUCCESS, + payload: { + schedule + } + } + expect(actions.scheduleStatusChanged(schedule)).toEqual(expectedResult) + }) + }) + describe('changeScheduleJobType', () => { + it('changeScheduleJobType should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CHANGE_SCHEDULE_JOB_TYPE, + payload: { + jobType + } + } + expect(actions.changeScheduleJobType(jobType)).toEqual(expectedResult) + }) + }) + describe('changeSchedulesStatusFail', () => { + it('changeSchedulesStatusFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.CHANGE_SCHEDULE_STATUS_FAILURE, + payload: {} + } + expect(actions.changeSchedulesStatusFail()).toEqual(expectedResult) + }) + }) + describe('executeScheduleImmediately', () => { + it('executeScheduleImmediately should return the correct type', () => { + const expectedResult = { + type: ActionTypes.EXECUTE_SCHEDULE_IMMEDIATELY, + payload: { + id: scheduleId, + resolve + } + } + expect(actions.executeScheduleImmediately(scheduleId, resolve)).toEqual(expectedResult) + }) + }) + describe('resetScheduleState', () => { + it('resetScheduleState should return the correct type', () => { + const expectedResult = { + type: ActionTypes.RESET_SCHEDULE_STATE, + payload: {} + } + expect(actions.resetScheduleState()).toEqual(expectedResult) + }) + }) + describe('loadSuggestMails', () => { + it('loadSuggestMails should return the correct type', () => { + const expectedResult = { type: ActionTypes.LOAD_SUGGEST_MAILS, payload: { + keyword: '' + } } + expect(actions.loadSuggestMails('')).toEqual(expectedResult) + }) + }) + describe('suggestMailsLoaded', () => { + it('suggestMailsLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_SUGGEST_MAILS_SUCCESS, + payload: { + mails + } + } + expect(actions.suggestMailsLoaded(mails)).toEqual(expectedResult) + }) + }) + describe('loadSuggestMailsFail', () => { + it('loadSuggestMailsFail should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_SUGGEST_MAILS_FAILURE, + payload: {} + } + expect(actions.loadSuggestMailsFail()).toEqual(expectedResult) + }) + }) + describe('portalDashboardsLoaded', () => { + it('portalDashboardsLoaded should return the correct type', () => { + const expectedResult = { + type: ActionTypes.LOAD_PORTAL_DASHBOARDS_SUCCESS, + payload: { + portalId: scheduleId, + dashboards: [] + } + } + expect(actions.portalDashboardsLoaded(scheduleId, [])).toEqual(expectedResult) + }) + }) + describe('loadVizs', () => { + it('loadVizs should return the correct type', () => { + const expectedResult = { type: ActionTypes.LOAD_VIZS, payload: { + projectId: scheduleId + } } + expect(actions.loadVizs(scheduleId)).toEqual(expectedResult) + }) + }) + describe('vizsLoaded', () => { + it('vizsLoaded should return the correct type', () => { + const expectedResult = { type: ActionTypes.LOAD_VIZS_SUCCESS, payload: { + result: schedule + } } + expect(actions.vizsLoaded(schedule)).toEqual(expectedResult) + }) + }) + describe('loadVizsFail', () => { + it('loadVizsFail should return the correct type', () => { + const expectedResult = { type: ActionTypes.LOAD_VIZS_FAILUER} + expect(actions.loadVizsFail()).toEqual(expectedResult) + }) + }) +}) diff --git a/webapp/test/app/containers/Schedule/fixtures.ts b/webapp/test/app/containers/Schedule/fixtures.ts new file mode 100644 index 000000000..e5ee3f837 --- /dev/null +++ b/webapp/test/app/containers/Schedule/fixtures.ts @@ -0,0 +1,77 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { + JobType, + IUserInfo, + ISchedule, + JobStatus +} from 'app/containers/Schedule/components/types' + +interface ImockStore { + schedule: ISchedule + projectId: number + schedules: ISchedule[] + scheduleId: number + resolve: () => void + jobType: JobType + mails: IUserInfo[] + keywords: string + jobStatus: JobStatus +} + +const scheduleDemo: ISchedule = { + id: 1, + name: 'scheduleName', + description: 'desc', + projectId: 2, + startDate: '', + endDate: '', + cronExpression: '', + jobStatus: 'new', + jobType: 'email', + execLog: '', + config: { + webHookUrl: 'string', + type: 'string', + imageWidth: 1, + contentList: [], + setCronExpressionManually: false + } +} + +export const mockStore: ImockStore = { + schedule: scheduleDemo, + projectId: 1, + schedules: [scheduleDemo], + jobStatus: scheduleDemo.jobStatus, + scheduleId: 2, + keywords: 'keywords', + resolve: () => void 0, + jobType: 'email', + mails: [ + { + id: 1, + username: '', + email: '', + avatar: '' + } + ] +} diff --git a/webapp/test/app/containers/Schedule/reducer.test.ts b/webapp/test/app/containers/Schedule/reducer.test.ts new file mode 100644 index 000000000..348fb1e7c --- /dev/null +++ b/webapp/test/app/containers/Schedule/reducer.test.ts @@ -0,0 +1,75 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import produce from 'immer' +import reducer, { initialState } from 'app/containers/Schedule/reducer' +import actions from 'app/containers/Schedule/actions' +import { mockAnonymousAction } from 'test/utils/fixtures' +import { mockStore } from './fixtures' + +describe('projectsReducer', () => { + const { scheduleId, schedules, schedule } = mockStore + let state + beforeEach(() => { + state = initialState + }) + + it('should return the initial state', () => { + expect(reducer(void 0, mockAnonymousAction)).toEqual(state) + }) + + it('should handle the loadSchedules action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.table = true + }) + expect(reducer(state, actions.loadSchedules(scheduleId))).toEqual( + expectedResult + ) + }) + + it('should handle the relRoleProjectLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.table = false + }) + expect(reducer(state, actions.schedulesLoaded(schedules))).toEqual( + expectedResult + ) + }) + + it('should handle the relRoleProjectLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.table = false + draft.schedules = draft.schedules.filter(({ id }) => id !== scheduleId) + }) + expect(reducer(state, actions.scheduleDeleted(scheduleId))).toEqual( + expectedResult + ) + }) + + it('should handle the scheduleAdded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.edit = false + draft.schedules.unshift(schedule) + }) + expect(reducer(state, actions.scheduleAdded(schedule))).toEqual( + expectedResult + ) + }) +}) diff --git a/webapp/test/app/containers/Schedule/sagas.test.ts b/webapp/test/app/containers/Schedule/sagas.test.ts new file mode 100644 index 000000000..c3bb76874 --- /dev/null +++ b/webapp/test/app/containers/Schedule/sagas.test.ts @@ -0,0 +1,187 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { throwError } from 'redux-saga-test-plan/providers' +import request from 'app/utils/request' +import actions from 'app/containers/Schedule/actions' +import { + getSchedules, + getScheduleDetail, + addSchedule, + deleteSchedule, + editSchedule, + getSuggestMails, + getVizsData, + changeScheduleStatus, + executeScheduleImmediately +} from 'app/containers/Schedule/sagas' +import { mockStore } from './fixtures' +import { getMockResponse } from 'test/utils/fixtures' + +describe('Schedule Sagas', () => { + const { schedule, projectId, schedules, keywords, jobStatus, mails } = mockStore + describe('getSchedules Saga', () => { + const getSchedulesActions = actions.loadSchedules(projectId) + it('should dispatch the schedulesLoaded action if it requests the data successfully', () => { + return expectSaga(getSchedules, getSchedulesActions) + .provide([[matchers.call.fn(request), getMockResponse(schedules)]]) + .put(actions.schedulesLoaded(schedules)) + .run() + }) + it('should call the loadSchedulesFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getSchedules, getSchedulesActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadSchedulesFail()) + .run() + }) + }) + + describe('getScheduleDetail Saga', () => { + const getScheduleDetailActions = actions.loadScheduleDetail(projectId) + it('should dispatch the scheduleDetailLoaded action if it requests the data successfully', () => { + return expectSaga(getScheduleDetail, getScheduleDetailActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) + .put(actions.scheduleDetailLoaded(schedule)) + .run() + }) + it('should call the loadScheduleDetailFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getScheduleDetail, getScheduleDetailActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadScheduleDetailFail()) + .run() + }) + }) + + describe('addSchedule Saga', () => { + const addScheduleActions = actions.addSchedule(schedule, () => void 0) + it('should dispatch the scheduleAdded action if it requests the data successfully', () => { + return expectSaga(getScheduleDetail, addScheduleActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) + .put(actions.scheduleAdded(schedule)) + .run() + }) + it('should call the addScheduleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getScheduleDetail, addScheduleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.addScheduleFail()) + .run() + }) + }) + + describe('deleteSchedule Saga', () => { + const deleteScheduleActions = actions.deleteSchedule(schedule.id) + it('should dispatch the scheduleDeleted action if it requests the data successfully', () => { + return expectSaga(getScheduleDetail, deleteScheduleActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule.id)]]) + .put(actions.scheduleDeleted(schedule.id)) + .run() + }) + it('should call the deleteScheduleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getScheduleDetail, deleteScheduleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.deleteScheduleFail()) + .run() + }) + }) + + describe('editSchedule Saga', () => { + const editScheduleActions = actions.editSchedule(schedule, () => void 0) + it('should dispatch the scheduleEdited action if it requests the data successfully', () => { + return expectSaga(getScheduleDetail, editScheduleActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) + .put(actions.scheduleEdited(schedule)) + .run() + }) + it('should call the editScheduleFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getScheduleDetail, editScheduleActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.editScheduleFail()) + .run() + }) + }) + + describe('getSuggestMails Saga', () => { + const loadSuggestMailsActions = actions.loadSuggestMails(keywords) + it('should dispatch the suggestMailsLoaded action if it requests the data successfully', () => { + return expectSaga(getSuggestMails, loadSuggestMailsActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) + .put(actions.suggestMailsLoaded(mails)) + .run() + }) + it('should call the loadSuggestMailsFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getSuggestMails, loadSuggestMailsActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadSuggestMailsFail()) + .run() + }) + }) + + describe('executeScheduleImmediately Saga', () => { + const executeScheduleImmediatelyActions = actions.executeScheduleImmediately(schedule.id, () => void 0) + it('should dispatch the executeScheduleImmediatelyActions action if it requests the data successfully', () => { + return expectSaga(executeScheduleImmediately, executeScheduleImmediatelyActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) + .run() + }) + }) + + describe('changeScheduleStatus Saga', () => { + const changeScheduleStatusActions = actions.changeSchedulesStatus(schedule.id, jobStatus) + it('should dispatch the scheduleStatusChanged action if it requests the data successfully', () => { + return expectSaga(changeScheduleStatus, changeScheduleStatusActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) + .put(actions.scheduleStatusChanged(schedule)) + .run() + }) + it('should call the changeSchedulesStatusFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(changeScheduleStatus, changeScheduleStatusActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.changeSchedulesStatusFail()) + .run() + }) + }) + + describe('getVizsData Saga', () => { + const loadVizsActions = actions.loadVizs(projectId) + it('should dispatch the vizsLoaded action if it requests the data successfully', () => { + return expectSaga(getVizsData, loadVizsActions) + .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) + .put(actions.vizsLoaded(schedule)) + .run() + }) + it('should call the loadVizsFail action if the response errors', () => { + const errors = new Error('error') + return expectSaga(getVizsData, loadVizsActions) + .provide([[matchers.call.fn(request), throwError(errors)]]) + .put(actions.loadVizsFail()) + .run() + }) + }) + +}) diff --git a/webapp/test/app/containers/Schedule/selectors.test.ts b/webapp/test/app/containers/Schedule/selectors.test.ts new file mode 100644 index 000000000..00b8cab32 --- /dev/null +++ b/webapp/test/app/containers/Schedule/selectors.test.ts @@ -0,0 +1,77 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import { + selectSchedule, + makeSelectSchedules, + makeSelectEditingSchedule, + makeSelectLoading, + makeSelectSuggestMails, + makeSelectPortalDashboards, + makeSelectVizs +} from 'app/containers/Schedule/selectors' +import { initialState } from 'app/containers/Schedule/reducer' + +const state = { + schedule: initialState +} + +describe('selectProject', () => { + it('should select the projects state', () => { + expect(selectSchedule(state)).toEqual(state.schedule) + }) +}) + +describe('makeSelectProjects', () => { + const scheduleSelector = makeSelectSchedules() + const editingScheduleSelector = makeSelectEditingSchedule() + const loadingSelector = makeSelectLoading() + const suggestMailsSelector = makeSelectSuggestMails() + const portalDashboardSelector = makeSelectPortalDashboards() + const vizsSelector = makeSelectVizs() + + it('should select the scheduleSelector', () => { + expect(scheduleSelector(state)).toEqual(state.schedule.schedules) + }) + + it('should select the editingScheduleSelector', () => { + expect(editingScheduleSelector(state)).toEqual( + state.schedule.editingSchedule + ) + }) + + it('should select the loadingSelector', () => { + expect(loadingSelector(state)).toEqual(state.schedule.loading) + }) + + it('should select the suggestMailsSelector', () => { + expect(suggestMailsSelector(state)).toEqual(state.schedule.suggestMails) + }) + + it('should select the portalDashboardSelector', () => { + expect(portalDashboardSelector(state)).toEqual( + state.schedule.portalDashboards + ) + }) + + it('should select the vizsSelector', () => { + expect(vizsSelector(state)).toEqual(state.schedule.vizs) + }) +}) diff --git a/webapp/test/app/containers/Source/sagas.test.ts b/webapp/test/app/containers/Source/sagas.test.ts index 4635ad429..ee984db6b 100644 --- a/webapp/test/app/containers/Source/sagas.test.ts +++ b/webapp/test/app/containers/Source/sagas.test.ts @@ -44,4 +44,5 @@ describe('Source Sagas', () => { .run() }) }) + }) diff --git a/webapp/test/utils/test-bundler.js b/webapp/test/utils/test-bundler.js index 41e71b593..fc280401b 100644 --- a/webapp/test/utils/test-bundler.js +++ b/webapp/test/utils/test-bundler.js @@ -1,3 +1,3 @@ // needed for regenerator-runtime // (ES7 generator support is required by redux-saga) -import 'babel-polyfill' +// import 'babel-polyfill' From 178c4fb5db01e94ad515766577cc4c4ac5b07f8f Mon Sep 17 00:00:00 2001 From: ruanhan1988 <2856197796@qq.com> Date: Mon, 19 Oct 2020 14:55:19 +0800 Subject: [PATCH 02/18] fix(test) getOrganization sagas organizationDetailLoaded --- webapp/test/app/containers/Organizations/saga.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/test/app/containers/Organizations/saga.test.ts b/webapp/test/app/containers/Organizations/saga.test.ts index 3ef879503..8b4b0ddb5 100644 --- a/webapp/test/app/containers/Organizations/saga.test.ts +++ b/webapp/test/app/containers/Organizations/saga.test.ts @@ -127,10 +127,10 @@ describe('Organizations Sagas', () => { describe('getOrganizationDetail Saga', () => { const loadOrganizationDetailActions = actions.loadOrganizationDetail(orgId) - it('should dispatch the organizationDeleted action if it requests the data successfully', () => { + it('should dispatch the organizationDetailLoaded action if it requests the data successfully', () => { return expectSaga(getOrganizationDetail, loadOrganizationDetailActions) .provide([[matchers.call.fn(request), getMockResponse(organization)]]) - .put(actions.organizationDetailLoaded(orgId)) + .put(actions.organizationDetailLoaded(organization)) .run() }) }) From 10d1e6502efef85b469d72dbbfd7f813ac07540c Mon Sep 17 00:00:00 2001 From: ruanhan1988 <2856197796@qq.com> Date: Mon, 19 Oct 2020 15:05:21 +0800 Subject: [PATCH 03/18] fix(test): fix schedule saga.test --- webapp/test/app/containers/Schedule/sagas.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/webapp/test/app/containers/Schedule/sagas.test.ts b/webapp/test/app/containers/Schedule/sagas.test.ts index c3bb76874..164060562 100644 --- a/webapp/test/app/containers/Schedule/sagas.test.ts +++ b/webapp/test/app/containers/Schedule/sagas.test.ts @@ -76,14 +76,14 @@ describe('Schedule Sagas', () => { describe('addSchedule Saga', () => { const addScheduleActions = actions.addSchedule(schedule, () => void 0) it('should dispatch the scheduleAdded action if it requests the data successfully', () => { - return expectSaga(getScheduleDetail, addScheduleActions) + return expectSaga(addSchedule, addScheduleActions) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) .put(actions.scheduleAdded(schedule)) .run() }) it('should call the addScheduleFail action if the response errors', () => { const errors = new Error('error') - return expectSaga(getScheduleDetail, addScheduleActions) + return expectSaga(addSchedule, addScheduleActions) .provide([[matchers.call.fn(request), throwError(errors)]]) .put(actions.addScheduleFail()) .run() @@ -93,14 +93,14 @@ describe('Schedule Sagas', () => { describe('deleteSchedule Saga', () => { const deleteScheduleActions = actions.deleteSchedule(schedule.id) it('should dispatch the scheduleDeleted action if it requests the data successfully', () => { - return expectSaga(getScheduleDetail, deleteScheduleActions) + return expectSaga(deleteSchedule, deleteScheduleActions) .provide([[matchers.call.fn(request), getMockResponse(schedule.id)]]) .put(actions.scheduleDeleted(schedule.id)) .run() }) it('should call the deleteScheduleFail action if the response errors', () => { const errors = new Error('error') - return expectSaga(getScheduleDetail, deleteScheduleActions) + return expectSaga(deleteSchedule, deleteScheduleActions) .provide([[matchers.call.fn(request), throwError(errors)]]) .put(actions.deleteScheduleFail()) .run() @@ -110,14 +110,14 @@ describe('Schedule Sagas', () => { describe('editSchedule Saga', () => { const editScheduleActions = actions.editSchedule(schedule, () => void 0) it('should dispatch the scheduleEdited action if it requests the data successfully', () => { - return expectSaga(getScheduleDetail, editScheduleActions) + return expectSaga(editSchedule, editScheduleActions) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) .put(actions.scheduleEdited(schedule)) .run() }) it('should call the editScheduleFail action if the response errors', () => { const errors = new Error('error') - return expectSaga(getScheduleDetail, editScheduleActions) + return expectSaga(editSchedule, editScheduleActions) .provide([[matchers.call.fn(request), throwError(errors)]]) .put(actions.editScheduleFail()) .run() From c1531bcdb2da267a9a938bfff98523add7733632 Mon Sep 17 00:00:00 2001 From: ruanhan1988 <2856197796@qq.com> Date: Mon, 19 Oct 2020 15:29:22 +0800 Subject: [PATCH 04/18] fix:(test) update jest.config.js --- webapp/jest.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/jest.config.js b/webapp/jest.config.js index 63c6d3c53..12317f655 100644 --- a/webapp/jest.config.js +++ b/webapp/jest.config.js @@ -29,7 +29,8 @@ module.exports = { '.*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/test/mocks/image.js', '^app/(.*)$': '/app/$1', - '^test/(.*)$': '/test/$1' + '^test/(.*)$': '/test/$1', + '^libs/(.*)$': '/libs/$1' }, setupFilesAfterEnv: [ // '/test/utils/test-bundler.js', From 381096655894dd1cb47b59550b7aefa435c97196 Mon Sep 17 00:00:00 2001 From: ruanhan1988 <2856197796@qq.com> Date: Mon, 19 Oct 2020 20:02:40 +0800 Subject: [PATCH 05/18] fix(test) fix schedule some tests in actions and reducers --- webapp/jest.config.js | 3 +- .../app/containers/Schedule/actions.test.ts | 143 ++++++++++++------ .../test/app/containers/Schedule/fixtures.ts | 17 ++- .../app/containers/Schedule/reducer.test.ts | 138 ++++++++++++++++- .../app/containers/Schedule/sagas.test.ts | 5 +- webapp/test/mocks/font.js | 1 + 6 files changed, 256 insertions(+), 51 deletions(-) create mode 100644 webapp/test/mocks/font.js diff --git a/webapp/jest.config.js b/webapp/jest.config.js index 12317f655..4fdaeca28 100644 --- a/webapp/jest.config.js +++ b/webapp/jest.config.js @@ -30,7 +30,8 @@ module.exports = { '/test/mocks/image.js', '^app/(.*)$': '/app/$1', '^test/(.*)$': '/test/$1', - '^libs/(.*)$': '/libs/$1' + '^libs/(.*)$': '/libs/$1', + '^assets/fonts/(.*)$': '/test/mocks/font.js' }, setupFilesAfterEnv: [ // '/test/utils/test-bundler.js', diff --git a/webapp/test/app/containers/Schedule/actions.test.ts b/webapp/test/app/containers/Schedule/actions.test.ts index ba505c444..64d363774 100644 --- a/webapp/test/app/containers/Schedule/actions.test.ts +++ b/webapp/test/app/containers/Schedule/actions.test.ts @@ -20,23 +20,37 @@ import { ActionTypes } from 'app/containers/Schedule/constants' import actions from 'app/containers/Schedule/actions' -import {mockStore} from './fixtures' +import { mockStore } from './fixtures' describe('Schedule Actions', () => { - const { schedule, projectId, mails, schedules, scheduleId, resolve, jobType } = mockStore + const { + schedule, + projectId, + mails, + schedules, + scheduleId, + resolve, + jobType + } = mockStore describe('loadSchedules', () => { it('loadSchedules should return the correct type', () => { - const expectedResult = { type: ActionTypes.LOAD_SCHEDULES, payload: { - projectId - } } + const expectedResult = { + type: ActionTypes.LOAD_SCHEDULES, + payload: { + projectId + } + } expect(actions.loadSchedules(projectId)).toEqual(expectedResult) }) }) describe('schedulesLoaded', () => { it('schedulesLoaded should return the correct type', () => { - const expectedResult = { type: ActionTypes.LOAD_SCHEDULES_SUCCESS, payload: { - schedules - } } + const expectedResult = { + type: ActionTypes.LOAD_SCHEDULES_SUCCESS, + payload: { + schedules + } + } expect(actions.schedulesLoaded(schedules)).toEqual(expectedResult) }) }) @@ -82,67 +96,91 @@ describe('Schedule Actions', () => { }) describe('addSchedule', () => { it('addSchedule should return the correct type', () => { - const expectedResult = { type: ActionTypes.ADD_SCHEDULE, payload: { - schedule, - resolve - } } + const expectedResult = { + type: ActionTypes.ADD_SCHEDULE, + payload: { + schedule, + resolve + } + } expect(actions.addSchedule(schedule, resolve)).toEqual(expectedResult) }) }) describe('scheduleAdded', () => { it('scheduleAdded should return the correct type', () => { - const expectedResult = { type: ActionTypes.ADD_SCHEDULE_SUCCESS, payload: { - result: schedule - } } + const expectedResult = { + type: ActionTypes.ADD_SCHEDULE_SUCCESS, + payload: { + result: schedule + } + } expect(actions.scheduleAdded(schedule)).toEqual(expectedResult) }) }) describe('addScheduleFail', () => { it('addScheduleFail should return the correct type', () => { - const expectedResult = { type: ActionTypes.ADD_SCHEDULE_FAILURE, payload: {} } + const expectedResult = { + type: ActionTypes.ADD_SCHEDULE_FAILURE, + payload: {} + } expect(actions.addScheduleFail()).toEqual(expectedResult) }) }) describe('editSchedule', () => { it('editSchedule should return the correct type', () => { - const expectedResult = { type: ActionTypes.EDIT_SCHEDULE, payload: { - schedule, - resolve - } } - expect(actions.editSchedule(schedule, - resolve)).toEqual(expectedResult) + const expectedResult = { + type: ActionTypes.EDIT_SCHEDULE, + payload: { + schedule, + resolve + } + } + expect(actions.editSchedule(schedule, resolve)).toEqual(expectedResult) }) }) describe('scheduleEdited', () => { it('scheduleEdited should return the correct type', () => { - const expectedResult = { type: ActionTypes.EDIT_SCHEDULE_SUCCESS, payload: { - result: schedule - } } + const expectedResult = { + type: ActionTypes.EDIT_SCHEDULE_SUCCESS, + payload: { + result: schedule + } + } expect(actions.scheduleEdited(schedule)).toEqual(expectedResult) }) }) describe('editScheduleFail', () => { it('editScheduleFail should return the correct type', () => { - const expectedResult = { type: ActionTypes.EDIT_SCHEDULE_FAILURE, payload: {} } + const expectedResult = { + type: ActionTypes.EDIT_SCHEDULE_FAILURE, + payload: {} + } expect(actions.editScheduleFail()).toEqual(expectedResult) }) }) describe('deleteSchedule', () => { it('deleteSchedule should return the correct type', () => { - const expectedResult = { type: ActionTypes.DELETE_SCHEDULE, payload: { - id: scheduleId - } } + const expectedResult = { + type: ActionTypes.DELETE_SCHEDULE, + payload: { + id: scheduleId + } + } expect(actions.deleteSchedule(scheduleId)).toEqual(expectedResult) }) }) describe('scheduleDeleted', () => { it('scheduleDeleted should return the correct type', () => { - const expectedResult = { type: ActionTypes.DELETE_SCHEDULE, payload: { - id: scheduleId - } } + const expectedResult = { + type: ActionTypes.DELETE_SCHEDULE_SUCCESS, + payload: { + id: scheduleId + } + } expect(actions.scheduleDeleted(scheduleId)).toEqual(expectedResult) }) }) + describe('deleteScheduleFail', () => { it('deleteScheduleFail should return the correct type', () => { const expectedResult = { @@ -161,7 +199,9 @@ describe('Schedule Actions', () => { currentStatus: 'new' } } - expect(actions.changeSchedulesStatus(scheduleId, 'new')).toEqual(expectedResult) + expect(actions.changeSchedulesStatus(scheduleId, 'new')).toEqual( + expectedResult + ) }) }) describe('scheduleStatusChanged', () => { @@ -204,7 +244,9 @@ describe('Schedule Actions', () => { resolve } } - expect(actions.executeScheduleImmediately(scheduleId, resolve)).toEqual(expectedResult) + expect(actions.executeScheduleImmediately(scheduleId, resolve)).toEqual( + expectedResult + ) }) }) describe('resetScheduleState', () => { @@ -218,9 +260,12 @@ describe('Schedule Actions', () => { }) describe('loadSuggestMails', () => { it('loadSuggestMails should return the correct type', () => { - const expectedResult = { type: ActionTypes.LOAD_SUGGEST_MAILS, payload: { - keyword: '' - } } + const expectedResult = { + type: ActionTypes.LOAD_SUGGEST_MAILS, + payload: { + keyword: '' + } + } expect(actions.loadSuggestMails('')).toEqual(expectedResult) }) }) @@ -253,28 +298,36 @@ describe('Schedule Actions', () => { dashboards: [] } } - expect(actions.portalDashboardsLoaded(scheduleId, [])).toEqual(expectedResult) + expect(actions.portalDashboardsLoaded(scheduleId, [])).toEqual( + expectedResult + ) }) }) describe('loadVizs', () => { it('loadVizs should return the correct type', () => { - const expectedResult = { type: ActionTypes.LOAD_VIZS, payload: { - projectId: scheduleId - } } + const expectedResult = { + type: ActionTypes.LOAD_VIZS, + payload: { + projectId: scheduleId + } + } expect(actions.loadVizs(scheduleId)).toEqual(expectedResult) }) }) describe('vizsLoaded', () => { it('vizsLoaded should return the correct type', () => { - const expectedResult = { type: ActionTypes.LOAD_VIZS_SUCCESS, payload: { - result: schedule - } } + const expectedResult = { + type: ActionTypes.LOAD_VIZS_SUCCESS, + payload: { + result: schedule + } + } expect(actions.vizsLoaded(schedule)).toEqual(expectedResult) }) }) describe('loadVizsFail', () => { it('loadVizsFail should return the correct type', () => { - const expectedResult = { type: ActionTypes.LOAD_VIZS_FAILUER} + const expectedResult = { type: ActionTypes.LOAD_VIZS_FAILUER } expect(actions.loadVizsFail()).toEqual(expectedResult) }) }) diff --git a/webapp/test/app/containers/Schedule/fixtures.ts b/webapp/test/app/containers/Schedule/fixtures.ts index e5ee3f837..873ac66c6 100644 --- a/webapp/test/app/containers/Schedule/fixtures.ts +++ b/webapp/test/app/containers/Schedule/fixtures.ts @@ -24,6 +24,7 @@ import { ISchedule, JobStatus } from 'app/containers/Schedule/components/types' +import { IDashboard } from 'app/containers/Dashboard/types' interface ImockStore { schedule: ISchedule @@ -35,6 +36,7 @@ interface ImockStore { mails: IUserInfo[] keywords: string jobStatus: JobStatus + dashboard: IDashboard } const scheduleDemo: ISchedule = { @@ -73,5 +75,18 @@ export const mockStore: ImockStore = { email: '', avatar: '' } - ] + ], + dashboard: { + id: 1, + name: 'string', + parentId: 1, + index: 1, + dashboardPortalId: 1, + type: 0, + config: { + filters: [], + linkages: [], + queryMode: 0 + } + } } diff --git a/webapp/test/app/containers/Schedule/reducer.test.ts b/webapp/test/app/containers/Schedule/reducer.test.ts index 348fb1e7c..12da5b5d6 100644 --- a/webapp/test/app/containers/Schedule/reducer.test.ts +++ b/webapp/test/app/containers/Schedule/reducer.test.ts @@ -21,11 +21,12 @@ import produce from 'immer' import reducer, { initialState } from 'app/containers/Schedule/reducer' import actions from 'app/containers/Schedule/actions' +import { EmptySchedule, EmptyWeChatWorkSchedule } from 'app/containers/Schedule/constants' import { mockAnonymousAction } from 'test/utils/fixtures' import { mockStore } from './fixtures' describe('projectsReducer', () => { - const { scheduleId, schedules, schedule } = mockStore + const { scheduleId, schedules, schedule, jobType, mails, dashboard } = mockStore let state beforeEach(() => { state = initialState @@ -44,9 +45,10 @@ describe('projectsReducer', () => { ) }) - it('should handle the relRoleProjectLoaded action correctly', () => { + it('should handle the schedulesLoaded action correctly', () => { const expectedResult = produce(state, (draft) => { draft.loading.table = false + draft.schedules = schedules }) expect(reducer(state, actions.schedulesLoaded(schedules))).toEqual( expectedResult @@ -72,4 +74,136 @@ describe('projectsReducer', () => { expectedResult ) }) + + it('should handle the deleteScheduleFail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.table = false + }) + expect(reducer(state, actions.deleteScheduleFail())).toEqual(expectedResult) + }) + + it('should handle the loadScheduleDetail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.schedule = true + }) + expect(reducer(state, actions.loadScheduleDetail(scheduleId))).toEqual( + expectedResult + ) + }) + + it('should handle the scheduleDetailLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.editingSchedule = schedule + draft.loading.schedule = false + }) + expect(reducer(state, actions.scheduleDetailLoaded(schedule))).toEqual( + expectedResult + ) + }) + + it('should handle the loadScheduleDetailFail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.schedule = false + }) + expect(reducer(state, actions.loadScheduleDetailFail())).toEqual( + expectedResult + ) + }) + + it('should handle the addSchedule and editSchedule action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.edit = true + }) + expect( + reducer( + state, + actions.addSchedule(schedule, () => void 0) + ) + ).toEqual(expectedResult) + expect( + reducer( + state, + actions.editSchedule(schedule, () => void 0) + ) + ).toEqual(expectedResult) + }) + + it('should handle the scheduleEdited action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.schedules.splice( + draft.schedules.findIndex(({ id }) => id === scheduleId), + 1, + schedule + ) + draft.loading.edit = false + }) + expect(reducer(state, actions.scheduleEdited(schedule))).toEqual( + expectedResult + ) + }) + + it('should handle the addScheduleFail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.loading.edit = false + }) + expect(reducer(state, actions.addScheduleFail())).toEqual( + expectedResult + ) + expect(reducer(state, actions.editScheduleFail())).toEqual( + expectedResult + ) + }) + + it('should handle the scheduleStatusChanged action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.schedules.splice( + draft.schedules.findIndex( + ({ id }) => id === scheduleId + ), + 1, + schedule + ) + }) + expect(reducer(state, actions.scheduleStatusChanged(schedule))).toEqual( + expectedResult + ) + }) + + it('should handle the changeScheduleJobType action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.editingSchedule = jobType === 'email' ? EmptySchedule : EmptyWeChatWorkSchedule + + }) + expect(reducer(state, actions.changeScheduleJobType(jobType))).toEqual( + expectedResult + ) + }) + + it('should handle the suggestMailsLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.suggestMails = mails + }) + expect(reducer(state, actions.suggestMailsLoaded(mails))).toEqual( + expectedResult + ) + }) + + it('should handle the loadSuggestMailsFail action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.suggestMails = [] + }) + expect(reducer(state, actions.loadSuggestMailsFail())).toEqual( + expectedResult + ) + }) + + it('should handle the portalDashboardsLoaded action correctly', () => { + const expectedResult = produce(state, (draft) => { + draft.portalDashboards[scheduleId] = [dashboard] + }) + expect(reducer(state, actions.portalDashboardsLoaded(scheduleId, [dashboard]))).toEqual( + expectedResult + ) + }) + }) diff --git a/webapp/test/app/containers/Schedule/sagas.test.ts b/webapp/test/app/containers/Schedule/sagas.test.ts index 164060562..4ae126f48 100644 --- a/webapp/test/app/containers/Schedule/sagas.test.ts +++ b/webapp/test/app/containers/Schedule/sagas.test.ts @@ -37,13 +37,14 @@ import { import { mockStore } from './fixtures' import { getMockResponse } from 'test/utils/fixtures' + describe('Schedule Sagas', () => { const { schedule, projectId, schedules, keywords, jobStatus, mails } = mockStore describe('getSchedules Saga', () => { const getSchedulesActions = actions.loadSchedules(projectId) it('should dispatch the schedulesLoaded action if it requests the data successfully', () => { return expectSaga(getSchedules, getSchedulesActions) - .provide([[matchers.call.fn(request), getMockResponse(schedules)]]) + .provide([[matchers.call.fn(request), getMockResponse(projectId)]]) .put(actions.schedulesLoaded(schedules)) .run() }) @@ -57,7 +58,7 @@ describe('Schedule Sagas', () => { }) describe('getScheduleDetail Saga', () => { - const getScheduleDetailActions = actions.loadScheduleDetail(projectId) + const getScheduleDetailActions = actions.loadScheduleDetail(schedule.id) it('should dispatch the scheduleDetailLoaded action if it requests the data successfully', () => { return expectSaga(getScheduleDetail, getScheduleDetailActions) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) diff --git a/webapp/test/mocks/font.js b/webapp/test/mocks/font.js new file mode 100644 index 000000000..4ba52ba2c --- /dev/null +++ b/webapp/test/mocks/font.js @@ -0,0 +1 @@ +module.exports = {} From c46a798f3f5e889cc8ad5660640944d303cfae2a Mon Sep 17 00:00:00 2001 From: ruanhan1988 <2856197796@qq.com> Date: Tue, 20 Oct 2020 11:12:21 +0800 Subject: [PATCH 06/18] fix:(test) fix shedule sagas --- .../test/app/containers/Schedule/fixtures.ts | 4 +- .../app/containers/Schedule/sagas.test.ts | 46 +++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/webapp/test/app/containers/Schedule/fixtures.ts b/webapp/test/app/containers/Schedule/fixtures.ts index 873ac66c6..5afa34496 100644 --- a/webapp/test/app/containers/Schedule/fixtures.ts +++ b/webapp/test/app/containers/Schedule/fixtures.ts @@ -37,6 +37,7 @@ interface ImockStore { keywords: string jobStatus: JobStatus dashboard: IDashboard + api: string } const scheduleDemo: ISchedule = { @@ -88,5 +89,6 @@ export const mockStore: ImockStore = { linkages: [], queryMode: 0 } - } + }, + api: '/api/v3/protal/projectId' } diff --git a/webapp/test/app/containers/Schedule/sagas.test.ts b/webapp/test/app/containers/Schedule/sagas.test.ts index 4ae126f48..de19a7110 100644 --- a/webapp/test/app/containers/Schedule/sagas.test.ts +++ b/webapp/test/app/containers/Schedule/sagas.test.ts @@ -21,6 +21,7 @@ import { expectSaga } from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' import { throwError } from 'redux-saga-test-plan/providers' +import {call} from 'redux-saga/effects' import request from 'app/utils/request' import actions from 'app/containers/Schedule/actions' import { @@ -37,15 +38,22 @@ import { import { mockStore } from './fixtures' import { getMockResponse } from 'test/utils/fixtures' - describe('Schedule Sagas', () => { - const { schedule, projectId, schedules, keywords, jobStatus, mails } = mockStore + const { + schedule, + projectId, + schedules, + keywords, + jobStatus, + mails, + api + } = mockStore describe('getSchedules Saga', () => { const getSchedulesActions = actions.loadSchedules(projectId) it('should dispatch the schedulesLoaded action if it requests the data successfully', () => { return expectSaga(getSchedules, getSchedulesActions) .provide([[matchers.call.fn(request), getMockResponse(projectId)]]) - .put(actions.schedulesLoaded(schedules)) + .dispatch(actions.schedulesLoaded(schedules)) .run() }) it('should call the loadSchedulesFail action if the response errors', () => { @@ -62,7 +70,7 @@ describe('Schedule Sagas', () => { it('should dispatch the scheduleDetailLoaded action if it requests the data successfully', () => { return expectSaga(getScheduleDetail, getScheduleDetailActions) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) - .put(actions.scheduleDetailLoaded(schedule)) + .dispatch(actions.scheduleDetailLoaded(schedule)) .run() }) it('should call the loadScheduleDetailFail action if the response errors', () => { @@ -79,7 +87,7 @@ describe('Schedule Sagas', () => { it('should dispatch the scheduleAdded action if it requests the data successfully', () => { return expectSaga(addSchedule, addScheduleActions) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) - .put(actions.scheduleAdded(schedule)) + .dispatch(actions.scheduleAdded(schedule)) .run() }) it('should call the addScheduleFail action if the response errors', () => { @@ -130,7 +138,7 @@ describe('Schedule Sagas', () => { it('should dispatch the suggestMailsLoaded action if it requests the data successfully', () => { return expectSaga(getSuggestMails, loadSuggestMailsActions) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) - .put(actions.suggestMailsLoaded(mails)) + .dispatch(actions.suggestMailsLoaded(mails)) .run() }) it('should call the loadSuggestMailsFail action if the response errors', () => { @@ -143,20 +151,29 @@ describe('Schedule Sagas', () => { }) describe('executeScheduleImmediately Saga', () => { - const executeScheduleImmediatelyActions = actions.executeScheduleImmediately(schedule.id, () => void 0) + const executeScheduleImmediatelyActions = actions.executeScheduleImmediately( + schedule.id, + () => void 0 + ) it('should dispatch the executeScheduleImmediatelyActions action if it requests the data successfully', () => { - return expectSaga(executeScheduleImmediately, executeScheduleImmediatelyActions) + return expectSaga( + executeScheduleImmediately, + executeScheduleImmediatelyActions + ) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) .run() }) }) describe('changeScheduleStatus Saga', () => { - const changeScheduleStatusActions = actions.changeSchedulesStatus(schedule.id, jobStatus) + const changeScheduleStatusActions = actions.changeSchedulesStatus( + schedule.id, + jobStatus + ) it('should dispatch the scheduleStatusChanged action if it requests the data successfully', () => { return expectSaga(changeScheduleStatus, changeScheduleStatusActions) .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) - .put(actions.scheduleStatusChanged(schedule)) + .dispatch(actions.scheduleStatusChanged(schedule)) .run() }) it('should call the changeSchedulesStatusFail action if the response errors', () => { @@ -172,10 +189,14 @@ describe('Schedule Sagas', () => { const loadVizsActions = actions.loadVizs(projectId) it('should dispatch the vizsLoaded action if it requests the data successfully', () => { return expectSaga(getVizsData, loadVizsActions) - .provide([[matchers.call.fn(request), getMockResponse(schedule)]]) - .put(actions.vizsLoaded(schedule)) + .provide([ + [call(request, api), getMockResponse(projectId)], + [matchers.call.fn(request), getMockResponse(projectId)] + ]) + .dispatch(actions.vizsLoaded(schedule)) .run() }) + it('should call the loadVizsFail action if the response errors', () => { const errors = new Error('error') return expectSaga(getVizsData, loadVizsActions) @@ -184,5 +205,4 @@ describe('Schedule Sagas', () => { .run() }) }) - }) From d298f02b5a66a8e76e0aa4919faca25385b063df Mon Sep 17 00:00:00 2001 From: ScottSut Date: Tue, 20 Oct 2020 22:45:42 +0800 Subject: [PATCH 07/18] refactor(Widget): Change the logic of auto merge cells to group merge(pivot style) --- .../Widget/components/Chart/Table/index.tsx | 10 +-- .../Widget/components/Chart/Table/util.ts | 72 ++++++++++++++----- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/webapp/app/containers/Widget/components/Chart/Table/index.tsx b/webapp/app/containers/Widget/components/Chart/Table/index.tsx index 5f5d3afdf..7c364449a 100644 --- a/webapp/app/containers/Widget/components/Chart/Table/index.tsx +++ b/webapp/app/containers/Widget/components/Chart/Table/index.tsx @@ -41,8 +41,8 @@ import { traverseConfig, computeCellWidth, getDataColumnWidth, - getMergedCellSpan, - getTableCellValueRange + getTableCellValueRange, + getCellSpanMap } from './util' import { MapAntSortOrder } from './constants' import { FieldSortTypes } from '../../Config/Sort' @@ -705,10 +705,12 @@ function getTableColumns(props: IChartProps) { const tableColumns: Array> = [] const mapTableHeaderConfig: IMapTableHeaderConfig = {} const fixedColumnInfo: { [key: string]: number } = {} + const dimensions = cols.concat(rows) + const cellSpanMap = getCellSpanMap(data, dimensions) let calculatedTotalWidth = 0 let fixedTotalWidth = 0 - cols.concat(rows).forEach((dimension) => { + dimensions.forEach((dimension) => { const { name, field, format } = dimension const headerText = getFieldAlias(field, queryVariables || {}) || name const column: ColumnProps = { @@ -729,7 +731,7 @@ function getTableColumns(props: IChartProps) { if (autoMergeCell) { column.render = (text, _, idx) => { // dimension cells needs merge - const rowSpan = getMergedCellSpan(data, name, idx) + const rowSpan = cellSpanMap[name][idx] return rowSpan === 1 ? text : { children: text, props: { rowSpan } } } } diff --git a/webapp/app/containers/Widget/components/Chart/Table/util.ts b/webapp/app/containers/Widget/components/Chart/Table/util.ts index be820ce98..ca8e3e677 100644 --- a/webapp/app/containers/Widget/components/Chart/Table/util.ts +++ b/webapp/app/containers/Widget/components/Chart/Table/util.ts @@ -21,6 +21,7 @@ import { ITableCellStyle, ITableColumnConfig, DefaultTableCellStyle } from 'containers/Widget/components/Config/Table' import { getTextWidth } from 'utils/util' import { IFieldFormatConfig, getFormattedValue } from 'containers/Widget/components/Config/Format' +import { IWidgetDimension } from '../../Widget' export function traverseConfig (config: T[], childrenName: keyof T, callback: (currentConfig: T, idx: number, siblings: T[]) => any) { if (!Array.isArray(config)) { return } @@ -69,22 +70,61 @@ export function textAlignAdapter (justifyContent: ITableCellStyle['justifyConten } } -export function getMergedCellSpan (data: any[], propName: string, idx: number) { - const currentRecord = data[idx] - const prevRecord = data[idx - 1] - if (prevRecord && prevRecord[propName] === currentRecord[propName]) { - return 0 - } - let span = 1 - while (true) { - const nextRecord = data[idx + span] - if (nextRecord && nextRecord[propName] === currentRecord[propName]) { - span++ - } else { - break - } - } - return span +export function getCellSpanMap( + data: any[], + dimensions: IWidgetDimension[] +): { [columnName: string]: number[] } { + const map = {} + const columnsSpanEndIndexRecorder = {} + + dimensions.forEach(({ name }) => { + map[name] = [] + columnsSpanEndIndexRecorder[name] = -1 + }) + + data.forEach((record, index) => { + const prevRecord = data[index - 1] + + dimensions.forEach(({ name }, dIndex) => { + const prevColumnName = dimensions[dIndex - 1]?.name + + if ( + columnsSpanEndIndexRecorder[name] !== index - 1 && + prevRecord && + prevRecord[name] === record[name] + ) { + map[name][index] = 0 + return + } + + if (columnsSpanEndIndexRecorder[prevColumnName] === index) { + map[name][index] = 1 + columnsSpanEndIndexRecorder[name] = index + return + } + + let span = 1 + while (true) { + const nextRecord = data[index + span] + const currentDataIndex = index + span - 1 + if ( + nextRecord && + nextRecord[name] === record[name] && + !( + prevColumnName && + currentDataIndex >= columnsSpanEndIndexRecorder[prevColumnName] + ) + ) { + span += 1 + } else { + map[name][index] = span + columnsSpanEndIndexRecorder[name] = currentDataIndex + break + } + } + }) + }) + return map } export function computeCellWidth (style: ITableCellStyle, cellValue: string | number) { From 8362ecd0e42e8d4726b384041c2807386c30861e Mon Sep 17 00:00:00 2001 From: zll <1060962187@qq.com> Date: Thu, 22 Oct 2020 19:21:22 +0800 Subject: [PATCH 08/18] fix: display preview bug --- .../app/containers/Display/components/Layer/RichText/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/app/containers/Display/components/Layer/RichText/index.tsx b/webapp/app/containers/Display/components/Layer/RichText/index.tsx index 508682d88..fe4438f17 100644 --- a/webapp/app/containers/Display/components/Layer/RichText/index.tsx +++ b/webapp/app/containers/Display/components/Layer/RichText/index.tsx @@ -59,7 +59,7 @@ const LabelEditor: React.FC = () => { ) useEffect(() => { - if (!editing) { + if (!editing && typeof editing === 'boolean') { dispatch( DisplayActions.editSlideLayerParams( layerId, From 298708a308972e1f4982406c6e0efd9768c1c1f6 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Thu, 22 Oct 2020 11:41:55 +0800 Subject: [PATCH 09/18] chore: Remove globalConfig --- webapp/app/containers/Background/index.tsx | 4 +- webapp/app/globalConfig.ts | 38 --------------- webapp/app/globalConstants.ts | 3 ++ webapp/app/utils/api.ts | 57 ++++++++++------------ 4 files changed, 32 insertions(+), 70 deletions(-) delete mode 100644 webapp/app/globalConfig.ts diff --git a/webapp/app/containers/Background/index.tsx b/webapp/app/containers/Background/index.tsx index 4ab85c3b4..1ad7f7228 100644 --- a/webapp/app/containers/Background/index.tsx +++ b/webapp/app/containers/Background/index.tsx @@ -26,7 +26,7 @@ import Login from 'containers/Login' import Register from 'containers/Register' import { makeSelectVersion } from 'containers/App/selectors' import JoinOrganization from 'containers/Register/JoinOrganization' -import { clientVersion } from 'app/globalConfig' +import { CLIENT_VERSION } from 'app/globalConstants' const styles = require('./Background.less') export const Background: FC = () => { @@ -36,7 +36,7 @@ export const Background: FC = () => {

{!version ? ( '' - ) : clientVersion !== version ? ( + ) : CLIENT_VERSION !== version ? ( 客户端与服务端版本不一致,请更新 diff --git a/webapp/app/globalConfig.ts b/webapp/app/globalConfig.ts deleted file mode 100644 index 98da99a40..000000000 --- a/webapp/app/globalConfig.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * << - * Davinci - * == - * Copyright (C) 2016 - 2017 EDP - * == - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * >> - */ - -export const envName = { - production: 'production', - dev: 'dev' -} - -export const env = envName.production -export const clientVersion = '0.3-beta.9' -export default { - dev: { - host: '/api/v3', - shareHost: '/share.html' - }, - production: { - // host: '/api/v1', - host: '/api/v3', - shareHost: '/share.html' - } -} diff --git a/webapp/app/globalConstants.ts b/webapp/app/globalConstants.ts index e2bd0eeef..29688107d 100644 --- a/webapp/app/globalConstants.ts +++ b/webapp/app/globalConstants.ts @@ -18,6 +18,9 @@ * >> */ +export const CLIENT_VERSION = '0.3-beta.9' +export const API_HOST = '/api/v3' + const defaultEchartsTheme = require('assets/json/echartsThemes/default.project.json') export const DEFAULT_ECHARTS_THEME = defaultEchartsTheme.theme export const DEFAULT_PRIMARY_COLOR = '#1B98E0' diff --git a/webapp/app/utils/api.ts b/webapp/app/utils/api.ts index 1f3f39bbd..bb9341707 100644 --- a/webapp/app/utils/api.ts +++ b/webapp/app/utils/api.ts @@ -18,37 +18,34 @@ * >> */ -import config, { env } from '../globalConfig' - -const host = config[env].host +import { API_HOST } from '../globalConstants' export default { - externalAuthProviders: `${host}/login/getOauth2Clients`, - tryExternalAuth: `${host}/login/externalLogin`, + externalAuthProviders: `${API_HOST}/login/getOauth2Clients`, + tryExternalAuth: `${API_HOST}/login/externalLogin`, externalLogout: `/login/oauth2/logout`, - login: `${host}/login`, - group: `${host}/groups`, - user: `${host}/users`, - changepwd: `${host}/changepwd`, - source: `${host}/sources`, - view: `${host}/views`, - // bizdata: `${host}/bizdatas`, - widget: `${host}/widgets`, - display: `${host}/displays`, - share: `${host}/share`, - checkName: `${host}/check`, - projectsCheckName: `${host}/check/`, - uploads: `${host}/uploads`, - schedule: `${host}/cronjobs`, - signup: `${host}/users`, - organizations: `${host}/organizations`, - checkNameUnique: `${host}/check`, - projects: `${host}/projects`, - teams: `${host}/teams`, - roles: `${host}/roles`, - portal: `${host}/dashboardPortals`, - star: `${host}/star`, - download: `${host}/download`, - buriedPoints: `${host}/statistic`, - version: `${host}/version` + login: `${API_HOST}/login`, + group: `${API_HOST}/groups`, + user: `${API_HOST}/users`, + changepwd: `${API_HOST}/changepwd`, + source: `${API_HOST}/sources`, + view: `${API_HOST}/views`, + widget: `${API_HOST}/widgets`, + display: `${API_HOST}/displays`, + share: `${API_HOST}/share`, + checkName: `${API_HOST}/check`, + projectsCheckName: `${API_HOST}/check/`, + uploads: `${API_HOST}/uploads`, + schedule: `${API_HOST}/cronjobs`, + signup: `${API_HOST}/users`, + organizations: `${API_HOST}/organizations`, + checkNameUnique: `${API_HOST}/check`, + projects: `${API_HOST}/projects`, + teams: `${API_HOST}/teams`, + roles: `${API_HOST}/roles`, + portal: `${API_HOST}/dashboardPortals`, + star: `${API_HOST}/star`, + download: `${API_HOST}/download`, + buriedPoints: `${API_HOST}/statistic`, + version: `${API_HOST}/version` } From f02cb52ba51e48b1ed9a668c084ac9382234e716 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Tue, 27 Oct 2020 09:43:22 +0800 Subject: [PATCH 10/18] refactor(control&share): support control manual option type cascading & add share expiration --- .../ConditionValuesControl/index.tsx | 4 +- .../GlobalControlRelatedViewForm.tsx | 160 +++---- .../LocalControlRelatedInfoForm.tsx | 9 +- .../Control/Config/ControlForm/ValueForm.tsx | 6 +- .../Control/Config/ControlForm/index.tsx | 15 +- .../Control/Config/OptionSettingForm.tsx | 7 +- .../app/components/Control/Config/index.tsx | 132 ++---- webapp/app/components/Control/Panel/index.tsx | 66 ++- webapp/app/components/Control/types.ts | 15 +- webapp/app/components/Control/util.ts | 250 +++++------ webapp/app/components/DataDrill/strategies.ts | 6 +- webapp/app/components/DataDrill/types.ts | 4 +- .../components/FullScreenPanel/Control.tsx | 10 +- .../app/components/FullScreenPanel/index.tsx | 9 +- webapp/app/components/Linkages/index.ts | 6 +- .../SharePanel/ConfigForm/AuthForm.tsx | 199 +++++++++ .../SharePanel/ConfigForm/BaseForm.tsx | 91 ++++ .../SharePanel/ConfigForm/UrlClipboard.tsx | 81 ++++ .../SharePanel/ConfigForm/index.tsx | 120 +++++ .../components/SharePanel/ConfigForm/util.ts | 37 ++ webapp/app/components/SharePanel/Ctrl.tsx | 16 +- .../app/components/SharePanel/ShareForm.tsx | 148 ------ .../app/components/SharePanel/SharePanel.less | 36 +- webapp/app/components/SharePanel/index.tsx | 420 ++++-------------- webapp/app/components/SharePanel/types.ts | 28 +- webapp/app/containers/ControlPanel/Global.tsx | 11 +- webapp/app/containers/ControlPanel/Local.tsx | 7 +- webapp/app/containers/Dashboard/Grid.tsx | 2 + .../app/containers/Dashboard/SharePanel.tsx | 85 ++-- webapp/app/containers/Dashboard/actions.ts | 32 +- .../Dashboard/components/DashboardItem.tsx | 9 +- webapp/app/containers/Dashboard/reducer.ts | 31 +- webapp/app/containers/Dashboard/sagas.ts | 46 +- webapp/app/containers/Dashboard/types.ts | 6 +- webapp/app/containers/Dashboard/util.ts | 175 +++++--- .../containers/Display/Editor/SharePanel.tsx | 47 +- webapp/app/containers/Display/actions.ts | 15 +- webapp/app/containers/Display/reducer.ts | 4 +- webapp/app/containers/Display/sagas.ts | 23 +- webapp/app/containers/View/reducer.ts | 25 +- webapp/app/containers/View/types.ts | 8 +- .../components/Workbench/Dropbox/index.tsx | 4 +- .../Workbench/FilterSettingForm.tsx | 6 +- webapp/app/containers/Widget/types.ts | 1 - webapp/app/globalConstants.ts | 1 + .../app/utils/migrationRecorders/control.ts | 40 +- webapp/share/containers/App/Interceptor.tsx | 7 +- webapp/share/containers/App/actions.ts | 8 +- webapp/share/containers/App/index.tsx | 4 +- webapp/share/containers/App/reducer.ts | 3 + webapp/share/containers/App/sagas.ts | 22 +- webapp/share/containers/App/selectors.ts | 8 + .../containers/Dashboard/FullScreenPanel.tsx | 6 +- webapp/share/containers/Dashboard/actions.ts | 25 +- webapp/share/containers/Dashboard/index.tsx | 81 ++-- webapp/share/containers/Dashboard/reducer.ts | 13 +- webapp/share/containers/Dashboard/sagas.ts | 71 +-- webapp/share/containers/Dashboard/types.ts | 14 +- webapp/share/containers/Dashboard/util.ts | 6 +- .../containers/Display/Reveal/Display.tsx | 10 +- webapp/share/containers/Display/actions.ts | 8 +- webapp/share/containers/Display/reducer.ts | 4 +- webapp/share/containers/Display/sagas.ts | 38 +- webapp/share/containers/Display/selectors.ts | 7 + 64 files changed, 1521 insertions(+), 1267 deletions(-) create mode 100644 webapp/app/components/SharePanel/ConfigForm/AuthForm.tsx create mode 100644 webapp/app/components/SharePanel/ConfigForm/BaseForm.tsx create mode 100644 webapp/app/components/SharePanel/ConfigForm/UrlClipboard.tsx create mode 100644 webapp/app/components/SharePanel/ConfigForm/index.tsx create mode 100644 webapp/app/components/SharePanel/ConfigForm/util.ts delete mode 100644 webapp/app/components/SharePanel/ShareForm.tsx diff --git a/webapp/app/components/ConditionValuesControl/index.tsx b/webapp/app/components/ConditionValuesControl/index.tsx index 025d5f307..ecd4558d1 100644 --- a/webapp/app/components/ConditionValuesControl/index.tsx +++ b/webapp/app/components/ConditionValuesControl/index.tsx @@ -4,7 +4,7 @@ import moment, { Moment } from 'moment' import OperatorTypes from 'utils/operatorTypes' import { Row, Col, Input, InputNumber, DatePicker, Button, Tag, Switch } from 'antd' - +import { DEFAULT_DATETIME_FORMAT } from 'app/globalConstants' import Styles from './ConditionValuesControl.less' export type ConditionValueTypes = string | number | boolean @@ -170,7 +170,7 @@ export class ConditionValuesControl extends React.PureComponent (e: RadioChangeEvent) => void } @@ -43,6 +44,7 @@ interface IGlobalControlRelatedViewFormProps { const GlobalControlRelatedViewForm: FC = ({ form, relatedViews, + controlType, optionWithVariable, onFieldTypeChange }) => { @@ -92,85 +94,85 @@ const GlobalControlRelatedViewForm: FC = ({

{relatedViews.length ? ( - relatedViews.map( - ({ id, name, fieldType, fields, models, variables }) => { - const isMultiple = Array.isArray(fields) - const fieldValues = - form.getFieldValue(`relatedViews[${id}].fields`) || [] - return ( -
-
-

{name}

- {getFieldDecorator( - `relatedViews[${id}].fieldType`, - {} - )( - - - 字段 - - - 变量 - - - )} -
- - - - {getFieldDecorator(`relatedViews[${id}].fields`, { - rules: [ - { - required: true, - message: `关联${ - fieldType === ControlFieldTypes.Column - ? '字段' - : '变量' - }不能为空` - }, - { validator: columnValidator } - ] - })( - - )} - - - + relatedViews.map(({ id, name, fieldType, models, variables }) => { + const isMultiple = + IS_RANGE_TYPE[controlType] && + fieldType === ControlFieldTypes.Variable + const fieldValues = + form.getFieldValue(`relatedViews[${id}].fields`) || [] + return ( +
+
+

{name}

+ {getFieldDecorator( + `relatedViews[${id}].fieldType`, + {} + )( + + + 字段 + + + 变量 + + + )}
- ) - } - ) + + + + {getFieldDecorator(`relatedViews[${id}].fields`, { + rules: [ + { + required: true, + message: `关联${ + fieldType === ControlFieldTypes.Column + ? '字段' + : '变量' + }不能为空` + }, + { validator: columnValidator } + ] + })( + + )} + + + +
+ ) + }) ) : ( )} diff --git a/webapp/app/components/Control/Config/ControlForm/LocalControlRelatedInfoForm.tsx b/webapp/app/components/Control/Config/ControlForm/LocalControlRelatedInfoForm.tsx index 800c2d229..db2c0b1e6 100644 --- a/webapp/app/components/Control/Config/ControlForm/LocalControlRelatedInfoForm.tsx +++ b/webapp/app/components/Control/Config/ControlForm/LocalControlRelatedInfoForm.tsx @@ -23,7 +23,7 @@ import { Form, Row, Col, Radio, Select, Divider } from 'antd' import { WrappedFormUtils } from 'antd/lib/form/Form' import { RadioChangeEvent } from 'antd/lib/radio' import { IFlatRelatedView } from './types' -import { ControlFieldTypes } from '../../constants' +import { ControlFieldTypes, ControlTypes, IS_RANGE_TYPE } from '../../constants' import { filterSelectOption } from 'app/utils/util' import { IViewModelProps } from 'app/containers/View/types' const FormItem = Form.Item @@ -34,6 +34,7 @@ const RadioButton = Radio.Button interface ILocalControlRelatedInfoFormProps { form: WrappedFormUtils relatedView: IFlatRelatedView + controlType: ControlTypes optionWithVariable: boolean onFieldTypeChange: (id: number) => (e: RadioChangeEvent) => void } @@ -41,12 +42,14 @@ interface ILocalControlRelatedInfoFormProps { const LocalControlRelatedInfoForm: FC = ({ form, relatedView, + controlType, optionWithVariable, onFieldTypeChange }) => { const { getFieldDecorator } = form - const { id, fields, fieldType, models, variables } = relatedView - const isMultiple = Array.isArray(fields) + const { id, fieldType, models, variables } = relatedView + const isMultiple = + IS_RANGE_TYPE[controlType] && fieldType === ControlFieldTypes.Variable const fieldValues = form.getFieldValue(`relatedViews[${id}].fields`) || [] const colSpan = { xxl: 12, xl: 18 } const itemCols = { diff --git a/webapp/app/components/Control/Config/ControlForm/ValueForm.tsx b/webapp/app/components/Control/Config/ControlForm/ValueForm.tsx index 7efe5a23b..c6dcbba43 100644 --- a/webapp/app/components/Control/Config/ControlForm/ValueForm.tsx +++ b/webapp/app/components/Control/Config/ControlForm/ValueForm.tsx @@ -44,7 +44,7 @@ import { ControlOptionTypes, ControlTypes } from '../../constants' -import { IControl, IControlOption, IControlRelatedField } from '../../types' +import { IControl, IControlOption } from '../../types' import { IViewBase, IFormedViews } from 'app/containers/View/types' import { OperatorTypesLocale } from 'app/utils/operatorTypes' import { getOperatorOptions } from '../../util' @@ -152,9 +152,7 @@ const ValueForm: FC = ({ title: '关联变量', key: 'variables', dataIndex: 'variables', - render: (variables) => - variables && - Object.values(variables).map((v: IControlRelatedField) => v.name) + render: (variables) => variables && Object.values(variables) }) } diff --git a/webapp/app/components/Control/Config/ControlForm/index.tsx b/webapp/app/components/Control/Config/ControlForm/index.tsx index 6b174eebe..5d17230f5 100644 --- a/webapp/app/components/Control/Config/ControlForm/index.tsx +++ b/webapp/app/components/Control/Config/ControlForm/index.tsx @@ -34,7 +34,11 @@ import { IControl, IControlOption } from '../../types' import { IViewBase, IFormedViews } from 'app/containers/View/types' import { IFlatRelatedItem, IFlatRelatedView } from './types' import { parseDefaultValue } from '../../util' -import { ControlPanelTypes } from '../../constants' +import { + ControlFieldTypes, + ControlPanelTypes, + IS_RANGE_TYPE +} from '../../constants' import styles from '../../Control.less' interface IControlFormProps extends FormComponentProps { @@ -104,9 +108,10 @@ class ControlForm extends PureComponent { ...values, [`relatedViews[${id}].fieldType`]: fieldType, [`relatedViews[${id}].fields`]: fields - ? Array.isArray(fields) - ? fields.map((f) => f.name) - : fields.name + ? IS_RANGE_TYPE[controlBase.type] && + fieldType === ControlFieldTypes.Variable + ? fields + : fields[0] : void 0 }), {} @@ -195,6 +200,7 @@ class ControlForm extends PureComponent { @@ -206,6 +212,7 @@ class ControlForm extends PureComponent { diff --git a/webapp/app/components/Control/Config/OptionSettingForm.tsx b/webapp/app/components/Control/Config/OptionSettingForm.tsx index 13119c39f..489899295 100644 --- a/webapp/app/components/Control/Config/OptionSettingForm.tsx +++ b/webapp/app/components/Control/Config/OptionSettingForm.tsx @@ -48,12 +48,7 @@ class OptionSettingForm extends PureComponent { } else { form.setFieldsValue({ ...values, - ...(optionWithVariable && - values.variables && - Object.entries(values.variables).reduce((obj, [viewId, field]) => { - obj[viewId] = field.name - return obj - }, {})) + ...(optionWithVariable && values.variables) }) } } diff --git a/webapp/app/components/Control/Config/index.tsx b/webapp/app/components/Control/Config/index.tsx index fbc48867a..d7c9bb726 100644 --- a/webapp/app/components/Control/Config/index.tsx +++ b/webapp/app/components/Control/Config/index.tsx @@ -21,10 +21,10 @@ import React, { PureComponent, GetDerivedStateFromProps } from 'react' import { IControl, - IControlRelatedField, IDistinctValueReqeustParams, IControlRelatedViewFormValue, - IControlOption + IControlOption, + IControlRelatedView } from '../types' import { getDefaultGlobalControl, @@ -242,10 +242,17 @@ export class ControlConfig extends PureComponent< formedViews } = this.props const { controls, editingControlBase } = this.state - const control = - type === ControlPanelTypes.Global - ? getDefaultGlobalControl() - : getDefaultLocalControl(formedViews[relatedViewId]) + + let control + if (type === ControlPanelTypes.Global) { + control = getDefaultGlobalControl() + } else { + if (relatedViewId && formedViews[relatedViewId]) { + control = getDefaultLocalControl(formedViews[relatedViewId]) + } else { + return + } + } if (editingControlBase) { this.mergeEditingControl((mergedControls) => { @@ -474,18 +481,14 @@ export class ControlConfig extends PureComponent< ? { ...relatedView, fieldType, - fields: this.getValidFields(type, fieldType, relatedView.fields) + fields: relatedView.fields } : relatedView }) this.setState({ editingRelatedViewList: changedRelatedViewList, controlFormWillChangeValues: { - [`relatedViews[${viewId}].fields`]: this.getValidFields( - type, - fieldType, - void 0 - ) + [`relatedViews[${viewId}].fields`]: [] } }) } @@ -545,7 +548,7 @@ export class ControlConfig extends PureComponent< ...rest, models: getRelatedViewModels(formedViews[rest.id], value), fieldType, - fields: this.getValidFields(value, fieldType, fields) + fields }) ), controlFormWillChangeValues: changedFields @@ -692,19 +695,15 @@ export class ControlConfig extends PureComponent< if (SHOULD_LOAD_OPTIONS[type] && !defaultValueLoading) { switch (optionType) { case ControlOptionTypes.Auto: - const relatedViewValues = values.relatedViews || {} + const relatedViewValues = this.convertFieldFormValues({ + ...values.relatedViews + }) const relatedViewMap = Object.entries(relatedViewValues) if (relatedViewMap.length) { const paramsByViewId = relatedViewMap.reduce( - ( - obj, - [viewId, { fieldType, fields }]: [ - string, - IControlRelatedViewFormValue - ] - ) => { + (obj, [viewId, { fieldType, fields }]) => { if (fieldType === ControlFieldTypes.Column) { - obj[viewId] = { columns: [fields] } + obj[viewId] = { columns: fields } } return obj }, @@ -718,7 +717,11 @@ export class ControlConfig extends PureComponent< if (options) { this.setState({ defaultValueOptions: transformOptions( - { ...editingControlBase, ...values }, + { + ...editingControlBase, + ...values, + relatedViews: relatedViewValues + }, options ) }) @@ -743,7 +746,11 @@ export class ControlConfig extends PureComponent< if (options) { this.setState({ defaultValueOptions: transformOptions( - { ...editingControlBase, ...values }, + { + ...editingControlBase, + ...values, + relatedViews: relatedViewValues + }, options ) }) @@ -788,26 +795,7 @@ export class ControlConfig extends PureComponent< ...(type === ControlPanelTypes.Global && { relatedItems: { ...relatedItems } }), - relatedViews: Object.entries({ ...relatedViews }).reduce( - ( - obj, - [viewId, { fieldType, fields }]: [ - string, - IControlRelatedViewFormValue - ] - ) => { - obj[viewId] = { - fieldType, - fields: Array.isArray(fields) - ? fields.map((field) => - this.convertFieldFormValues(viewId, fieldType, field) - ) - : this.convertFieldFormValues(viewId, fieldType, fields) - } - return obj - }, - {} - ) + relatedViews: this.convertFieldFormValues({ ...relatedViews }) } } else { return control @@ -817,22 +805,19 @@ export class ControlConfig extends PureComponent< }) } - private convertFieldFormValues = ( - viewId: string, - fieldType: ControlFieldTypes, - fields: string - ): IControlRelatedField => { - const { editingRelatedViewList } = this.state - const { models, variables } = editingRelatedViewList.find( - (rv) => rv.id === Number(viewId) + private convertFieldFormValues = (relatedViewFormValues: { + [viewId: string]: IControlRelatedViewFormValue + }): { [viewId: string]: IControlRelatedView } => { + return Object.entries(relatedViewFormValues).reduce( + (obj, [viewId, { fieldType, fields }]) => { + obj[viewId] = { + fieldType, + fields: [].concat(fields) + } + return obj + }, + {} ) - return { - name: fields, - type: - fieldType === ControlFieldTypes.Column - ? models.find((m) => m.name === fields).sqlType - : variables.find((v) => v.name === fields).valueType - } } private save = () => { @@ -853,26 +838,6 @@ export class ControlConfig extends PureComponent< }) } - private getValidFields = ( - type: ControlTypes, - fieldType: ControlFieldTypes, - fields: IControlRelatedField | IControlRelatedField[] - ): IControlRelatedField | IControlRelatedField[] => { - const shouldBeArrayFields = - IS_RANGE_TYPE[type] && fieldType === ControlFieldTypes.Variable - return fields - ? shouldBeArrayFields - ? Array.isArray(fields) - ? fields - : [fields] - : Array.isArray(fields) - ? fields[0] - : fields - : shouldBeArrayFields - ? [] - : void 0 - } - private openOptionModal = (index?) => { const { customOptions } = this.state.editingControlBase this.setState({ @@ -907,16 +872,13 @@ export class ControlConfig extends PureComponent< } = this.state let customOptions = editingControlBase.customOptions || [] - const editingOption = { + const editingOption: IControlOption = { value: values.value, text: values.text || values.value, ...(editingControlBase.optionWithVariable && { - variables: editingRelatedViewList.reduce((obj, { id, variables }) => { + variables: editingRelatedViewList.reduce((obj, { id }) => { if (values[id]) { - const { name, valueType } = variables.find( - (v) => v.name === values[id] - ) - obj[id] = { name, type: valueType } + obj[id] = values[id] } return obj }, {}) diff --git a/webapp/app/components/Control/Panel/index.tsx b/webapp/app/components/Control/Panel/index.tsx index caeaf684a..62cccaa87 100644 --- a/webapp/app/components/Control/Panel/index.tsx +++ b/webapp/app/components/Control/Panel/index.tsx @@ -23,16 +23,16 @@ import { IControl, OnGetControlOptions, IMapControlOptions, - IRenderTreeItem, - IControlRelatedField + IRenderTreeItem } from '../types' +import { IFormedViews, IShareFormedViews } from 'app/containers/View/types' import { - getVariableValue, - getModelValue, + getVariableParams, + getFilterParams, getAllChildren, getParents, getPanelRenderState, - getCustomOptionVariableValues, + getCustomOptionVariableParams, cleanInvisibleConditionalControlValues } from '../util' import { @@ -50,6 +50,7 @@ import FullScreenControlPanelLayout from './Layouts/FullScreen' interface IControlPanelProps { controls: IControl[] + formedViews: IFormedViews | IShareFormedViews items: string type: ControlPanelTypes layoutType: ControlPanelLayoutTypes @@ -147,7 +148,7 @@ class ControlPanel extends PureComponent< flatTree: { [key: string]: IRenderTreeItem }, controlValues: { [key: string]: any } ) => { - const { type, items, onGetOptions } = this.props + const { formedViews, type, items, onGetOptions } = this.props const { key, parent, @@ -173,6 +174,7 @@ class ControlPanel extends PureComponent< const parents = getParents(parent, flatTree) const requestParams = {} + // get cascading conditions Object.entries(relatedViews).forEach(([viewId, relatedView]) => { let filters = [] let variables = [] @@ -183,17 +185,41 @@ class ControlPanel extends PureComponent< Object.entries(parentControl.relatedViews).forEach( ([parentViewId, parentRelatedView]) => { if (viewId === parentViewId) { + let cascadeRelatedViewId: string | number + let cascadeRelatedFields: string[] + + switch (optionType) { + case ControlOptionTypes.Auto: + cascadeRelatedViewId = viewId + cascadeRelatedFields = parentRelatedView.fields + break + case ControlOptionTypes.Manual: + if (valueViewId === Number(parentViewId)) { + cascadeRelatedViewId = parentViewId + cascadeRelatedFields = parentRelatedView.fields + } else if ( + parentControl.optionType === ControlOptionTypes.Manual && + valueViewId === parentControl.valueViewId + ) { + cascadeRelatedViewId = parentControl.valueViewId + cascadeRelatedFields = [parentControl.valueField] + } + break + } + if ( - optionType === ControlOptionTypes.Auto || - (optionType === ControlOptionTypes.Manual && - valueViewId === Number(parentViewId)) + cascadeRelatedViewId && + cascadeRelatedFields && + formedViews[cascadeRelatedViewId] ) { + const { model, variable } = formedViews[cascadeRelatedViewId] if (parentControl.optionWithVariable) { variables = variables.concat( - getCustomOptionVariableValues( + getCustomOptionVariableParams( parentControl, - Number(parentViewId), - parentValue + Number(cascadeRelatedViewId), + parentValue, + variable ) ) } else { @@ -201,18 +227,20 @@ class ControlPanel extends PureComponent< parentRelatedView.fieldType === ControlFieldTypes.Column ) { filters = filters.concat( - getModelValue( + getFilterParams( parentControl, - parentRelatedView.fields as IControlRelatedField, - parentValue + cascadeRelatedFields, + parentValue, + model ) ) } else { variables = variables.concat( - getVariableValue( + getVariableParams( parentControl, - parentRelatedView.fields, - parentValue + cascadeRelatedFields, + parentValue, + variable ) ) } @@ -227,7 +255,7 @@ class ControlPanel extends PureComponent< case ControlOptionTypes.Auto: if (relatedView.fieldType === ControlFieldTypes.Column) { requestParams[viewId] = { - columns: [(relatedView.fields as IControlRelatedField).name], + columns: relatedView.fields, filters, variables, cache, diff --git a/webapp/app/components/Control/types.ts b/webapp/app/components/Control/types.ts index 8e5e07ad5..ecd1782b8 100644 --- a/webapp/app/components/Control/types.ts +++ b/webapp/app/components/Control/types.ts @@ -28,8 +28,6 @@ import { } from './constants' import { OperatorTypes } from 'utils/operatorTypes' import { IQueryConditions } from 'containers/Dashboard/types' -import { SqlTypes } from 'app/globalConstants' -import { ViewVariableValueTypes } from 'app/containers/View/constants' export interface IControlRelatedItem { viewId: number @@ -38,7 +36,7 @@ export interface IControlRelatedItem { export interface IControlRelatedView { fieldType: ControlFieldTypes - fields: IControlRelatedField | IControlRelatedField[] + fields: string[] } export interface IControlRelatedViewFormValue { @@ -46,16 +44,11 @@ export interface IControlRelatedViewFormValue { fields: string | string[] } -export interface IControlRelatedField { - name: string - type: SqlTypes | ViewVariableValueTypes -} - export interface IControlOption { text: string value: string variables?: { - [viewId: string]: IControlRelatedField + [viewId: string]: string } } @@ -138,11 +131,11 @@ export interface IMapControlOptions { [controlKey: string]: object[] } -export interface IFilters { +export interface IFilter { name: string type: string value: string[] | string operator: string sqlType: string - children?: IFilters + children?: IFilter } diff --git a/webapp/app/components/Control/util.ts b/webapp/app/components/Control/util.ts index 1a6b293ad..9a0b98294 100644 --- a/webapp/app/components/Control/util.ts +++ b/webapp/app/components/Control/util.ts @@ -23,8 +23,7 @@ import { IControl, IControlRelatedView, IRenderTreeItem, - IFilters, - IControlRelatedField, + IFilter, IControlOption, IControlCondition } from './types' @@ -47,7 +46,8 @@ import { IFormedView, IViewModelProps, IViewVariable, - IFormedViews + IFormedViews, + IViewModel } from 'app/containers/View/types' import { ViewVariableValueTypes, @@ -98,64 +98,65 @@ export function getDefaultLocalControl(view: IFormedView): IControl { relatedViews: { [view.id]: { fieldType: ControlFieldTypes.Column, - fields: defaultFields && { - name: defaultFields[0], - type: defaultFields[1].sqlType - } + fields: defaultFields && [defaultFields[0]] } } } return control } -export function getVariableValue( +export function getVariableParams( control: IControl, - fields: IControlRelatedField | IControlRelatedField[], - value + fields: string[], + value, + variables: IViewVariable[] ) { const { type, dateFormat, multiple } = control - let name - let valueType - let variable = [] + const fieldsVariables = fields + .map((name) => variables.find((v) => v.name === name)) + .filter((f) => !!f) + let params = [] if ( value === void 0 || value === null || - (typeof value === 'string' && !value.trim()) + (typeof value === 'string' && !value.trim()) || + !fieldsVariables.length ) { - return variable - } - - if (!Array.isArray(fields)) { - name = fields.name - valueType = fields.type + return params } switch (type) { case ControlTypes.InputText: case ControlTypes.Radio: - variable.push({ name, value: getValidVariableValue(value, valueType) }) + params = fieldsVariables.map(({ name, valueType }) => ({ + name, + value: getValidVariableValue(value, valueType) + })) break case ControlTypes.Select: case ControlTypes.TreeSelect: if (multiple) { if (value.length && value.length > 0) { - variable.push({ + params = fieldsVariables.map(({ name, valueType }) => ({ name, value: value .map((val) => getValidVariableValue(val, valueType)) .join(',') - }) + })) } } else { - variable.push({ name, value: getValidVariableValue(value, valueType) }) + params = fieldsVariables.map(({ name, valueType }) => ({ + name, + value: getValidVariableValue(value, valueType) + })) } break case ControlTypes.NumberRange: case ControlTypes.Slider: - variable = value.reduce((arr, val, index) => { + params = value.reduce((arr, val, index) => { if (val !== '' && !isNaN(val)) { - const { name, type: valueType } = fields[index] + const { name, valueType } = fieldsVariables[index] return arr.concat({ name, value: getValidVariableValue(val, valueType) @@ -166,96 +167,104 @@ export function getVariableValue( break case ControlTypes.Date: if (multiple) { - variable.push({ + params = fieldsVariables.map(({ name }) => ({ name, value: value .split(',') .map((v) => `'${v}'`) .join(',') - }) + })) } else { - variable.push({ name, value: `'${moment(value).format(dateFormat)}'` }) + params = fieldsVariables.map(({ name }) => ({ + name, + value: `'${moment(value).format(dateFormat)}'` + })) } break case ControlTypes.DateRange: if (value.length) { - variable = value.map((v, index) => { - const { name } = fields[index] - return { name, value: `'${moment(v).format(dateFormat)}'` } + params = value.map((v, index) => { + const { name } = fieldsVariables[index] + return { + name, + value: `'${moment(v).format(dateFormat)}'` + } }) } break default: const val = value.target.value.trim() if (val) { - variable.push({ name, value: getValidVariableValue(val, valueType) }) + params = fieldsVariables.map(({ name, valueType }) => ({ + name, + value: getValidVariableValue(val, valueType) + })) } break } - return variable + return params } -export function getCustomOptionVariableValues( +export function getCustomOptionVariableParams( control: IControl, viewId: number, - value + value, + variables: IViewVariable[] ): QueryVariable { const { customOptions } = control - let variables = [] + let params = [] if ( value === void 0 || value === null || (typeof value === 'string' && !value.trim()) ) { - return variables + return params } - if (Array.isArray(value)) { - value.forEach((val) => { + Array.from([]) + .concat(value) + .forEach((val) => { const selectedOption = customOptions.find((o) => o.value === val) if (selectedOption && selectedOption.variables[viewId]) { - variables = variables.concat( - getVariableValue( + params = params.concat( + getVariableParams( { ...control, multiple: false }, - selectedOption.variables[viewId], - val + [selectedOption.variables[viewId]], + val, + variables ) ) } }) - } else { - const selectedOption = customOptions.find((o) => o.value === value) - if (selectedOption && selectedOption.variables[viewId]) { - variables = variables.concat( - getVariableValue(control, selectedOption.variables[viewId], value) - ) - } - } - return variables + return params } // 全局过滤器 与 本地控制器 filter 操作 -export function getModelValue( +export function getFilterParams( control: IControl, - field: IControlRelatedField, - value + fields: string[], + value, + models: IViewModel ) { const { type, dateFormat, multiple, operator } = control // select '' true in - const { name, type: sqlType } = field + // filter is related with only one field + const filterFieldName = fields[0] const filters = [] if ( value === void 0 || value === null || - (typeof value === 'string' && !value.trim()) + (typeof value === 'string' && !value.trim()) || + !models[filterFieldName] ) { return filters } - const commanFilterJson: IFilters = { - name, + const { sqlType } = models[filterFieldName] + const filterBase: IFilter = { + name: filterFieldName, type: 'filter', value: getValidColumnValue(value, sqlType), sqlType, @@ -264,88 +273,59 @@ export function getModelValue( switch (type) { case ControlTypes.InputText: case ControlTypes.Radio: - filters.push(commanFilterJson) + filters.push(filterBase) break case ControlTypes.Select: case ControlTypes.TreeSelect: if (multiple) { if (Array.isArray(value) && value.length > 0) { - const filterJson = { - ...commanFilterJson, + filters.push({ + ...filterBase, value: value.map((val) => getValidColumnValue(val, sqlType)) - } - filters.push(filterJson) + }) } } else { - filters.push(commanFilterJson) + filters.push(filterBase) } break case ControlTypes.NumberRange: case ControlTypes.Slider: - if (value[0] !== '' && !isNaN(value[0])) { - const filterJson = { - ...commanFilterJson, - operator: '>=', - value: getValidColumnValue(value[0], sqlType) - } - filters.push(filterJson) - } - if (value[1] !== '' && !isNaN(value[1])) { - const filterJson = { - ...commanFilterJson, - operator: '<=', - value: getValidColumnValue(value[1], sqlType) + value.forEach((val, index) => { + if (val !== '' && !isNaN(val)) { + filters.push({ + ...filterBase, + operator: !index ? '>=' : '<=', + value: getValidColumnValue(val, sqlType) + }) } - filters.push(filterJson) - } + }) break case ControlTypes.Date: - if (multiple) { - const filterJson = { - ...commanFilterJson, - value: value - .split(',') - .map((val) => getValidColumnValue(val, sqlType)) - } - filters.push(filterJson) - } else { - const filterJson = { - ...commanFilterJson, - value: getValidColumnValue(moment(value).format(dateFormat), sqlType) - } - filters.push(filterJson) - } + filters.push({ + ...filterBase, + value: multiple + ? value.split(',').map((val) => getValidColumnValue(val, sqlType)) + : getValidColumnValue(moment(value).format(dateFormat), sqlType) + }) break case ControlTypes.DateRange: if (value.length) { - const filterJson1 = { - ...commanFilterJson, - operator: '>=', - value: getValidColumnValue( - moment(value[0]).format(dateFormat), - sqlType - ) - } - const filterJson2 = { - ...commanFilterJson, - operator: '<=', - value: getValidColumnValue( - moment(value[1]).format(dateFormat), - sqlType - ) - } - filters.push(filterJson1) - filters.push(filterJson2) + value.forEach((val, index) => { + filters.push({ + ...filterBase, + operator: !index ? '>=' : '<=', + value: getValidColumnValue(moment(val).format(dateFormat), sqlType) + }) + }) } break default: const inputValue = value.target.value.trim() - const filterJson = { - ...commanFilterJson, - value: getValidColumnValue(inputValue, sqlType) - } if (inputValue) { - filters.push(filterJson) + filters.push({ + ...filterBase, + value: getValidColumnValue(inputValue, sqlType) + }) } break } @@ -476,11 +456,7 @@ export function transformOptions( return Object.values( options.reduce((obj, o) => { Object.values(control.relatedViews).forEach(({ fields }) => { - const fieldName = - typeof fields === 'string' - ? (fields as string) - : (fields as IControlRelatedField).name - const value = o[fieldName] + const value = o[fields[0]] if (value !== void 0 && !obj[value]) { obj[value] = { value, text: value } } @@ -632,32 +608,6 @@ export function getParents( return parents } -export function getDefaultFields( - type: ControlTypes, - fieldType: ControlFieldTypes, - models: IViewModelProps[], - variables: IViewVariable[] -) { - if (fieldType === ControlFieldTypes.Column) { - return models.length - ? { - name: models[0].name, - type: models[0].sqlType - } - : void 0 - } else { - if (variables.length) { - const field = { - name: variables[0].name, - type: variables[0].valueType - } - return IS_RANGE_TYPE[type] ? [field] : field - } else { - return IS_RANGE_TYPE[type] ? [] : void 0 - } - } -} - export function getRelatedViewModels( view: IFormedView, type: ControlTypes diff --git a/webapp/app/components/DataDrill/strategies.ts b/webapp/app/components/DataDrill/strategies.ts index 97ace96f0..76cc9871e 100644 --- a/webapp/app/components/DataDrill/strategies.ts +++ b/webapp/app/components/DataDrill/strategies.ts @@ -32,7 +32,7 @@ import WidgetAbstract, { ISourceDataFilter } from './types' -import { IFilters } from '../Control/types' +import { IFilter } from '../Control/types' import { getValidColumnValue } from 'app/components/Control/util' @@ -478,7 +478,7 @@ function collectKeyValue(sourceDataFilter) { }, {}) } -function mappingFilters(sourceDataFilter, group): IFilters[] { +function mappingFilters(sourceDataFilter, group): IFilter[] { const mappgingSource = sourceDataFilter.map((source) => source && source[group] ? source[group] : source ) @@ -501,7 +501,7 @@ function getSqlType(target: string) { )(target) } -function combineFilters(keyValuds): IFilters[] { +function combineFilters(keyValuds): IFilter[] { return Object.keys(keyValuds).reduce((iteratee, target) => { const sqlType = getSqlType(target) return iteratee.concat({ diff --git a/webapp/app/components/DataDrill/types.ts b/webapp/app/components/DataDrill/types.ts index 7404a2c26..6bc9c0ea6 100644 --- a/webapp/app/components/DataDrill/types.ts +++ b/webapp/app/components/DataDrill/types.ts @@ -22,7 +22,7 @@ import { IViewModel } from 'containers/View/types' import { Merge } from 'utils/types' -import { IFilters } from 'app/components/Control/types' +import { IFilter } from 'app/components/Control/types' import { IQueryVariableMap } from 'containers/Dashboard/types' @@ -107,7 +107,7 @@ export default class WidgetAbstract { export interface IDrillDetail { type: DrillType groups: string[] - filters: IFilters[] + filters: IFilter[] currentGroup: string // 对应原 name [WidgetDimensions.COL]?: IWidgetDimension[] [WidgetDimensions.ROW]?: IWidgetDimension[] diff --git a/webapp/app/components/FullScreenPanel/Control.tsx b/webapp/app/components/FullScreenPanel/Control.tsx index 339abe681..792584a80 100644 --- a/webapp/app/components/FullScreenPanel/Control.tsx +++ b/webapp/app/components/FullScreenPanel/Control.tsx @@ -24,8 +24,12 @@ import GlobalControlPanel from 'containers/ControlPanel/Global' import LocalControlPanel from 'containers/ControlPanel/Local' import { IDashboardItem, IDashboard } from '../../containers/Dashboard/types' import { IWidgetFormed } from 'app/containers/Widget/types' -import { ControlPanelLayoutTypes, ControlPanelTypes } from 'app/components/Control/constants' +import { + ControlPanelLayoutTypes, + ControlPanelTypes +} from 'app/components/Control/constants' import { OnGetControlOptions } from 'app/components/Control/types' +import { IFormedViews, IShareFormedViews } from 'app/containers/View/types' import styles from './FullScreenPanel.less' interface IControlProps { @@ -36,6 +40,7 @@ interface IControlProps { hasLocalControls: boolean currentDashboard: IDashboard currentItems: IDashboardItem[] + formedViews: IFormedViews | IShareFormedViews onGetOptions: OnGetControlOptions onSearch: ( type: ControlPanelTypes, @@ -55,6 +60,7 @@ const FullScreenControl: React.FC = memo( hasLocalControls, currentDashboard, currentItems, + formedViews, onGetOptions, onSearch, onMonitoredSearchDataAction @@ -111,6 +117,7 @@ const FullScreenControl: React.FC = memo( = memo( /> ) : ( - } + formedViews: IFormedViews | IShareFormedViews currentItems: IDashboardItem[] currentItemsInfo: { [itemId: string]: IDashboardItemInfo | IShareDashboardItemInfo @@ -181,6 +177,7 @@ const FullScreenPanel: React.FC = memo( hasLocalControls={hasLocalControls} currentDashboard={currentDashboard} currentItems={currentItems} + formedViews={formedViews} onGetOptions={onGetOptions} onSearch={onSearch} onMonitoredSearchDataAction={onMonitoredSearchDataAction} diff --git a/webapp/app/components/Linkages/index.ts b/webapp/app/components/Linkages/index.ts index 4c0712384..bf46de7b7 100644 --- a/webapp/app/components/Linkages/index.ts +++ b/webapp/app/components/Linkages/index.ts @@ -1,7 +1,7 @@ import { QueryVariable } from 'containers/Dashboard/types' import { DEFAULT_SPLITER, SQL_NUMBER_TYPES } from 'app/globalConstants' import OperatorType from 'utils/operatorTypes' -import { IFilters } from 'app/components/Control/types' +import { IFilter } from 'app/components/Control/types' import {getValidColumnValue} from 'app/components/Control/util' export type LinkageType = 'column' | 'variable' @@ -61,7 +61,7 @@ export function processLinkage (itemId: number, triggerData, mappingLinkage: IMa Object.keys(mappingLinkage).forEach((linkagerItemId) => { const linkage = mappingLinkage[+linkagerItemId] - const linkageFilters: IFilters[] = [] + const linkageFilters: IFilter[] = [] const linkageVariables: QueryVariable = [] linkage.forEach((l) => { const { triggerKey, triggerSqlType, triggerType, linkagerKey, linkagerSqlType, linkagerType, relation } = l @@ -75,7 +75,7 @@ export function processLinkage (itemId: number, triggerData, mappingLinkage: IMa ? linkagerKey.replace(/\w+\((\w+)\)/, '$1') : linkagerKey - const filterJson: IFilters = { + const filterJson: IFilter = { name : validLinkagerKey, type: 'filter', value: interactValue, diff --git a/webapp/app/components/SharePanel/ConfigForm/AuthForm.tsx b/webapp/app/components/SharePanel/ConfigForm/AuthForm.tsx new file mode 100644 index 000000000..b499c48c3 --- /dev/null +++ b/webapp/app/components/SharePanel/ConfigForm/AuthForm.tsx @@ -0,0 +1,199 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import React, { + useState, + useCallback, + useEffect, + ReactElement, + memo +} from 'react' +import { debounce } from 'lodash' +import BaseForm from './BaseForm' +import { Form, Row, Col, Select, Radio } from 'antd' +import { WrappedFormUtils } from 'antd/lib/form/Form' +import { IProjectRoles } from 'containers/Organizations/component/ProjectRole' +import { IOrganizationMember } from 'containers/Organizations/types' +import { sliceLength } from './util' +import { TCopyType } from '../types' +import styles from '../SharePanel.less' +const FormItem = Form.Item +const SelectOption = Select.Option +const RadioGroup = Radio.Group + +interface IAuthFormProps { + form: WrappedFormUtils + projectRoles: IProjectRoles[] + organizationMembers: IOrganizationMember[] + shareUrl: string + loading: boolean + onSetRoles: (roles: number[]) => void + onSetViewers: (viewers: number[]) => void + onCopy: (copytype: TCopyType) => () => void + onGetToken: () => void +} + +const AuthForm: React.FC = ({ + form, + projectRoles, + organizationMembers, + shareUrl, + loading, + onSetRoles, + onSetViewers, + onGetToken, + onCopy +}) => { + const [authOptions, setAuthOptions] = useState([]) + const { getFieldDecorator } = form + + const getRoleOptions = useCallback( + (orgRoles: IProjectRoles[], searchValue: string) => { + return orgRoles + .filter((role: IProjectRoles) => + searchValue ? role.name.includes(searchValue.trim()) : role + ) + .map(({ id, name }) => ( + +
+ 角色 -{name} +
+
+ )) + }, + [] + ) + + const getOrgMemberOptions = useCallback( + (orgMembers: IOrganizationMember[], searchValue: string) => { + return orgMembers + .filter((member: IOrganizationMember) => + searchValue + ? member.user.username.includes(searchValue.trim()) + : member + ) + .map(({ id, user }: IOrganizationMember) => ( + +
+ 用户 - + + {user.username} + {user.email} + +
+
+ )) + }, + [] + ) + + const getOptions = useCallback( + (projectRoles, organizationMembers, searchValue) => { + const roles = getRoleOptions(projectRoles, searchValue) + const viewers = getOrgMemberOptions(organizationMembers, searchValue) + return sliceLength(100, roles, viewers)() + }, + [] + ) + + const debouncedSearch = useCallback( + debounce((searchValue: string) => { + setAuthOptions(getOptions(projectRoles, organizationMembers, searchValue)) + }, 500), + [projectRoles, organizationMembers] + ) + + useEffect(() => { + setAuthOptions(getOptions(projectRoles, organizationMembers, '')) + }, [projectRoles, organizationMembers]) + + const resetOptions = useCallback(() => { + setAuthOptions(getOptions(projectRoles, organizationMembers, '')) + }, [projectRoles, organizationMembers]) + + const change = useCallback( + (val: string[]) => { + const wrapper: { viewers: number[]; roles: number[] } = val.reduce( + (iteratee, target) => { + const [key, value] = target.split('-') + iteratee[key].push(Number(value)) + return iteratee + }, + { viewers: [], roles: [] } + ) + onSetRoles(wrapper.roles) + onSetViewers(wrapper.viewers) + }, + [onSetRoles, onSetViewers] + ) + + const itemStyle = { labelCol: { span: 6 }, wrapperCol: { span: 17 } } + + return ( + <> + + + + {getFieldDecorator('permission', { initialValue: 'SHARER' })( + + + 与分享者一致 + + + 使用被分享者自身权限 + + + )} + + + + + + + {getFieldDecorator('authorized', { + rules: [{ required: true, message: '被授权人不能为空' }] + })( + + )} + + + + + + ) +} + +export default memo(AuthForm) diff --git a/webapp/app/components/SharePanel/ConfigForm/BaseForm.tsx b/webapp/app/components/SharePanel/ConfigForm/BaseForm.tsx new file mode 100644 index 000000000..7e456ddcb --- /dev/null +++ b/webapp/app/components/SharePanel/ConfigForm/BaseForm.tsx @@ -0,0 +1,91 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import React, { memo, useCallback } from 'react' +import moment from 'moment' +import UrlClipboard from './UrlClipboard' +import { Form, Row, Col, DatePicker, Button } from 'antd' +import { WrappedFormUtils } from 'antd/lib/form/Form' +import { TCopyType } from '../types' +import { DEFAULT_DATETIME_FORMAT } from 'app/globalConstants' +import styles from '../SharePanel.less' +const FormItem = Form.Item + +interface IBaseFormProps { + form: WrappedFormUtils + shareUrl: string + password?: string + loading: boolean + onCopy: (copytype: TCopyType) => () => void + onGetToken: () => void +} + +const BaseForm: React.FC = ({ + form, + shareUrl, + password, + loading, + onCopy, + onGetToken +}) => { + const { getFieldDecorator } = form + const itemStyle = { labelCol: { span: 6 }, wrapperCol: { span: 17 } } + + const disabledDate = useCallback( + (current) => current && current < moment().subtract(1, 'day').endOf('day'), + [] + ) + + return ( + <> + + + + {getFieldDecorator('expired', { + rules: [{ required: true, message: '有效期不能为空' }] + })( + + )} + + + + + + + + + + + ) +} + +export default memo(BaseForm) diff --git a/webapp/app/components/SharePanel/ConfigForm/UrlClipboard.tsx b/webapp/app/components/SharePanel/ConfigForm/UrlClipboard.tsx new file mode 100644 index 000000000..dd88ef8fd --- /dev/null +++ b/webapp/app/components/SharePanel/ConfigForm/UrlClipboard.tsx @@ -0,0 +1,81 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import React, { memo } from 'react' +import { Form, Input, Row, Col, Button } from 'antd' +import { TCopyType } from '../types' +import styles from '../SharePanel.less' +const FormItem = Form.Item + +interface IUrlClipboardProps { + shareUrl: string + password?: string + onCopy: (copytype: TCopyType) => () => void +} + +const UrlClipboard: React.FC = ({ + shareUrl, + password, + onCopy +}) => { + const itemStyle = { labelCol: { span: 6 }, wrapperCol: { span: 17 } } + return ( + shareUrl && ( + <> + + + + + 复制链接 + + } + /> + + + + {password && ( + <> + + + + + 复制链接及口令 + + } + /> + + + + + )} + + ) + ) +} + +export default memo(UrlClipboard) diff --git a/webapp/app/components/SharePanel/ConfigForm/index.tsx b/webapp/app/components/SharePanel/ConfigForm/index.tsx new file mode 100644 index 000000000..1ec35bda9 --- /dev/null +++ b/webapp/app/components/SharePanel/ConfigForm/index.tsx @@ -0,0 +1,120 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +import React, { useCallback, forwardRef, useImperativeHandle } from 'react' +import { Form, message } from 'antd' +import BaseForm from './BaseForm' +import AuthForm from './AuthForm' +import { copyTextToClipboard } from '../utils' +import { TCopyType, Tmode } from '../types' +import { IProjectRoles } from 'containers/Organizations/component/ProjectRole' +import { IOrganizationMember } from 'containers/Organizations/types' +import { FormComponentProps } from 'antd/lib/form' +import styles from '../SharePanel.less' + +interface IConfigFormProps extends FormComponentProps { + mode: Tmode + shareUrl: string + password: string + loading: boolean + projectRoles: IProjectRoles[] + organizationMembers: IOrganizationMember[] + onSetRoles: (roles: number[]) => void + onSetViewers: (viewers: number[]) => void + onGetToken: () => void +} + +const ConfigForm: React.FC = ( + { + form, + mode, + shareUrl, + loading, + password, + projectRoles, + organizationMembers, + onSetRoles, + onSetViewers, + onGetToken + }, + ref +) => { + useImperativeHandle(ref, () => ({ form })) + + const copy = useCallback( + (copytype: TCopyType) => () => { + const text = + copytype === 'link' ? shareUrl : `链接:${shareUrl} 口令:${password}` + copyTextToClipboard( + text, + () => message.success('复制链接成功'), + () => message.warning('复制链接失败,请稍后重试') + ) + }, + [shareUrl, password] + ) + + let content + + switch (mode) { + case 'NORMAL': + content = ( + + ) + break + case 'PASSWORD': + content = ( + + ) + break + case 'AUTH': + content = ( + + ) + break + } + + return
{content}
+} + +export default Form.create()(forwardRef(ConfigForm)) diff --git a/webapp/app/components/SharePanel/ConfigForm/util.ts b/webapp/app/components/SharePanel/ConfigForm/util.ts new file mode 100644 index 000000000..45350b613 --- /dev/null +++ b/webapp/app/components/SharePanel/ConfigForm/util.ts @@ -0,0 +1,37 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2017 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + */ + +export function sliceLength(length: number, arr1: T[], arr2: U[]) { + let loop = true + return () => { + return new Array(length) + .fill(0) + .map(() => { + if (loop) { + loop = false + return arr1.length ? arr1.shift() : arr2.shift() + } else { + loop = true + return arr2.length ? arr2.shift() : arr1.shift() + } + }) + .filter((unEmpty) => unEmpty) + } +} diff --git a/webapp/app/components/SharePanel/Ctrl.tsx b/webapp/app/components/SharePanel/Ctrl.tsx index 26234a6e6..b8ac7ea1a 100644 --- a/webapp/app/components/SharePanel/Ctrl.tsx +++ b/webapp/app/components/SharePanel/Ctrl.tsx @@ -20,23 +20,27 @@ import React, { useCallback } from 'react' import styles from './SharePanel.less' -import {Radio} from 'antd' -import {RadioChangeEvent} from 'antd/lib/radio' +import { Radio } from 'antd' +import { RadioChangeEvent } from 'antd/lib/radio' const RadioButton = Radio.Button const RadioGroup = Radio.Group -import {ICtrl} from './types' +import { ICtrl } from './types' -const Contrl: React.FC = ({ mode, setSType }) => { +const Contrl: React.FC = ({ mode, onModeChange }) => { const radioChange = useCallback( (e: RadioChangeEvent) => { - setSType(e.target.value) + onModeChange(e.target.value) }, [mode] ) return (
- + 普通分享 口令分享 授权分享 diff --git a/webapp/app/components/SharePanel/ShareForm.tsx b/webapp/app/components/SharePanel/ShareForm.tsx deleted file mode 100644 index cfa568f4a..000000000 --- a/webapp/app/components/SharePanel/ShareForm.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* - * << - * Davinci - * == - * Copyright (C) 2016 - 2017 EDP - * == - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * >> - */ - -import React, { useRef, useMemo, useCallback, ReactElement } from 'react' - -import { Input, Row, Col, Button, message } from 'antd' -import config, { env } from 'app/globalConfig' -// FIXME -const apiHost = `${location.origin}${config[env].host}` -const shareHost = `${location.origin}${config[env].shareHost}` -const styles = require('./SharePanel.less') -import { copyTextToClipboard } from './utils' -import { TCopyType } from './types' -interface IShareFormProps { - type: string - shareToken: string - pwd?: string -} -const ShareForm: React.FC = ({ type, shareToken, pwd }) => { - const resolve = useCallback(() => message.success('复制链接成功'), []) - - const reject = useCallback( - () => message.warning('复制链接失败,请稍后重试'), - [] - ) - - const copy = useCallback( - (copytype: TCopyType) => () => { - const text = - copytype === 'link' ? linkValue : `链接: ${linkValue} 口令: ${pwd}` - copyTextToClipboard(text, resolve, reject) - }, - [type, shareToken, pwd] - ) - - const linkValue = useMemo(() => { - let linkValue = '' - - switch (type) { - case 'dashboard': - linkValue = `${shareHost}?shareToken=${encodeURI( - shareToken - )}&type=dashboard#share/dashboard` - break - case 'widget': - linkValue = `${shareHost}?shareToken=${encodeURI( - shareToken - )}&type=widget#share/dashboard` - break - case 'display': - linkValue = `${shareHost}?shareToken=${encodeURI( - shareToken - )}&type=display#share/display` - break - default: - break - } - return linkValue - }, [type, shareToken]) - - const Content: ReactElement = useMemo(() => { - if (pwd && pwd.length) { - return ( - <> - - - - - 复制链接 - - - } - readOnly - /> - - - - - - 口令: - - } - readOnly - /> - - - - - - - ) - } else { - return ( - - - - - 复制链接 - - - } - readOnly - /> - - - ) - } - }, [pwd, shareToken, type]) - - return Content -} - -export default ShareForm diff --git a/webapp/app/components/SharePanel/SharePanel.less b/webapp/app/components/SharePanel/SharePanel.less index 74d97593c..d93ba76a9 100644 --- a/webapp/app/components/SharePanel/SharePanel.less +++ b/webapp/app/components/SharePanel/SharePanel.less @@ -8,41 +8,25 @@ .panelContent { padding: 16px 0 4px 0; - text-align: center; + .footer { margin-top: 16px; } } -} - -.shareRow { - margin-bottom: 16px; - - &:last-child { - margin-bottom: 0; - } - .shareText { - text-align: right; - line-height: 32px; - display: block; - } - - .shareInput { - height: 32px; + :global { + .ant-form-item { + margin-bottom: 8px; + } } } -.authRadio { - padding-top: 10px!important; - display: flex!important; - justify-content: flex-start!important; +.generate { + margin-bottom: 8px; } -.authButton { - margin-left: -6px; -} -.pwdButton { - margin-left: 2px; + +.copy { + cursor: pointer; } .options { diff --git a/webapp/app/components/SharePanel/index.tsx b/webapp/app/components/SharePanel/index.tsx index fb5744113..747419e42 100644 --- a/webapp/app/components/SharePanel/index.tsx +++ b/webapp/app/components/SharePanel/index.tsx @@ -18,26 +18,18 @@ * >> */ -import React, { - useCallback, - useState, - useMemo, - useEffect, - ReactElement -} from 'react' -import { compose } from 'redux' -import { Icon, Button, Row, Col, Modal, Select, Radio } from 'antd' -import { RadioChangeEvent } from 'antd/lib/radio' +import React, { useCallback, useState, useMemo, createRef } from 'react' +import moment from 'moment' +import ConfigForm from './ConfigForm' +import { Modal } from 'antd' +import { FormComponentProps } from 'antd/lib/form/Form' import Ctrl from './Ctrl' -const SelectOption = Select.Option -const RadioGroup = Radio.Group -import { uuid } from 'utils/util' -import ShareForm from './ShareForm' -import styles from './SharePanel.less' -import { Tmode, TShareVizsType, TPermission, IGetTokenParams } from './types' +import { Tmode, TShareVizsType, IShareTokenParams } from './types' import { IProjectRoles } from 'containers/Organizations/component/ProjectRole' import { IOrganizationMember } from 'containers/Organizations/types' -import { debounce } from 'lodash' +import { DEFAULT_DATETIME_FORMAT, SHARE_HOST } from 'app/globalConstants' +import styles from './SharePanel.less' + interface ISharePanelProps { visible: boolean id: number @@ -45,15 +37,15 @@ interface ISharePanelProps { type: TShareVizsType title: string shareToken: string - pwdToken: string - pwd: string + passwordShareToken: string + password: string authorizedShareToken: string loading: boolean projectRoles: IProjectRoles[] organizationMembers: IOrganizationMember[] - onLoadDashboardShareLink?: (params: IGetTokenParams) => void - onLoadWidgetShareLink?: (params: IGetTokenParams) => void - onLoadDisplayShareLink?: (params: IGetTokenParams) => void + onLoadDashboardShareLink?: (params: IShareTokenParams) => void + onLoadWidgetShareLink?: (params: IShareTokenParams) => void + onLoadDisplayShareLink?: (params: IShareTokenParams) => void onClose: () => void } @@ -66,8 +58,8 @@ const SharePanel: React.FC = (props) => { loading, onClose, visible, - pwdToken, - pwd, + passwordShareToken, + password, shareToken, projectRoles, organizationMembers, @@ -76,331 +68,72 @@ const SharePanel: React.FC = (props) => { onLoadDisplayShareLink, onLoadDashboardShareLink } = props - const [mode, setShareType] = useState('NORMAL') - const [permission, setPermission] = useState('SHARER') - const [searchValue, setSearchValue] = useState('') - const [viewerIds, setViewerIds] = useState() + const [mode, setMode] = useState('NORMAL') + const [viewers, setViewers] = useState() const [roles, setRoles] = useState() - - useEffect(() => { - if (id && visible && !shareToken) { - getShareToken(type, { - id, - itemId, - mode, - permission, - roles, - viewerIds - }) - } - }, [ - id, - type, - visible, - shareToken, - itemId, - mode, - permission, - roles, - viewerIds - ]) - - useEffect(() => { - if (id && visible && !pwdToken && mode === 'PASSWORD') { - getShareToken(type, { - id, - itemId, - mode, - permission, - roles, - viewerIds - }) - } - }, [ - pwdToken, - type, - pwd, - id, - visible, - mode, - itemId, - permission, - roles, - viewerIds - ]) - - const getShareToken = (type: TShareVizsType, params: IGetTokenParams) => { - switch (type) { - case 'dashboard': - onLoadDashboardShareLink(params) - break - case 'widget': - onLoadWidgetShareLink(params) - break - case 'display': - onLoadDisplayShareLink(params) - default: - break - } - } - - const reloadShareToken = () => { - getShareToken(type, { id, itemId, mode, permission, roles, viewerIds }) - } - - const afterModalClose = () => { - setShareType('NORMAL') - } - - const requestToken = () => { - getShareToken(type, { id, itemId, mode, permission, roles, viewerIds }) - } - - const Regular: ReactElement = useMemo(() => { - return shareToken ? ( - - ) : ( - - ) - }, [id, itemId, type, shareToken]) - - const Pwd: ReactElement = useMemo(() => { - return pwdToken ? ( - - ) : ( - - ) - }, [id, itemId, type, pwd, pwdToken]) - - const AuthOptions = useMemo(() => { - const roles = compose( - setRoleOptions, - getOrgRoleBySearch - )(projectRoles || []) - const viewerIds = compose( - setMemberOptions, - getOrgMembersBysearch - )(organizationMembers || []) - - return sliceLength(100, roles, viewerIds)() - - function sliceLength(length: number, arr1: T[], arr2: U[]) { - let loop = true - return () => { - return new Array(length) - .fill(0) - .map(() => { - if (loop) { - loop = false - return arr1.length ? arr1.shift() : arr2.shift() - } else { - loop = true - return arr2.length ? arr2.shift() : arr1.shift() - } - }) - .filter((unEmpty) => unEmpty) + const configForm = createRef() + + const getShareToken = useCallback(() => { + configForm.current.form.validateFieldsAndScroll((err, values) => { + if (!err) { + const { permission } = values + const expired = moment(values.expired).format(DEFAULT_DATETIME_FORMAT) + const params = { id, itemId, mode, expired, permission, roles, viewers } + switch (type) { + case 'dashboard': + onLoadDashboardShareLink(params) + break + case 'widget': + onLoadWidgetShareLink(params) + break + case 'display': + onLoadDisplayShareLink(params) + default: + break + } } - } + }) + }, [id, itemId, mode, type, roles, viewers, configForm]) - function getOrgMembersBysearch( - orgMembers: IOrganizationMember[] - ): IOrganizationMember[] { - return orgMembers.filter((member: IOrganizationMember) => { - return searchValue && searchValue.length - ? member?.user?.username?.indexOf(searchValue.trim()) > -1 - : member - }) - } - - function getOrgRoleBySearch(orgRoles: IProjectRoles[]): IProjectRoles[] { - return orgRoles.filter((role: IProjectRoles) => { - return searchValue && searchValue.length - ? role && role.name.indexOf(searchValue.trim()) > -1 - : role - }) - } - - function setRoleOptions(orgRoles: IProjectRoles[]): ReactElement[] { - return orgRoles && orgRoles.length - ? orgRoles.map((role: IProjectRoles) => ( - -
- 角色 - - {role.name} -
-
- )) - : [] - } - - function setMemberOptions( - orgMembers: IOrganizationMember[] - ): ReactElement[] { - return orgMembers && orgMembers.length - ? orgMembers.map((member: IOrganizationMember) => ( - -
- 用户 - - - {member.user.username} - {member.user.email} - -
-
- )) - : [] - } - }, [searchValue, projectRoles, organizationMembers]) - - const debouncedSearch = useCallback( - debounce((searchValue: string) => { - setSearchValue(searchValue) - }, 500), - [searchValue] - ) - - const save = useCallback( - (vals: string[]) => { - const wrapper: { viewerIds: number[]; roles: number[] } = vals.reduce( - (iteratee, target) => { - const [key, value] = target.split('-') - iteratee[key].push(Number(value)) - return iteratee - }, - { viewerIds: [], roles: [] } - ) - setViewerIds(wrapper.viewerIds) - setRoles(wrapper.roles) - }, - [viewerIds, roles] - ) - - const change = useCallback( - (val) => { - save(val) - }, - [viewerIds, roles] - ) - - const changePermission = (event: RadioChangeEvent) => { - setPermission(event.target.value) + const afterModalClose = () => { + setMode('NORMAL') } - const authButton = useMemo(() => { - const isAuthorizedCanSend: boolean = !(roles?.length || viewerIds?.length) - return ( - - ) - }, [id, itemId, mode, permission, roles, viewerIds, type]) - - const Auth: ReactElement = useMemo(() => { - if (authorizedShareToken) { - return - } else { - return ( - <> - - - - - - - - - - 分享者权限 - - - 被分享者权限 - - - - {authButton} - - - ) + const modeChange = useCallback((val: Tmode) => { + if (val === 'AUTH') { + setViewers([]) + setRoles([]) } - }, [ - id, - itemId, - shareToken, - type, - permission, - mode, - roles, - viewerIds, - AuthOptions, - authorizedShareToken - ]) + setMode(val) + }, []) - const Content = useMemo(() => { - if (loading) { - return - } + const shareUrl = useMemo(() => { + let token = '' switch (mode) { case 'NORMAL': - return Regular - case 'AUTH': - return Auth + token = shareToken + break case 'PASSWORD': - return Pwd - default: - return Regular + token = passwordShareToken + break + case 'AUTH': + token = authorizedShareToken + break } - }, [ - id, - itemId, - permission, - type, - loading, - mode, - roles, - viewerIds, - AuthOptions - ]) - const setSType = useCallback( - (val: Tmode) => { - if (val === 'AUTH') { - setViewerIds([]) - setRoles([]) + if (token) { + switch (type) { + case 'dashboard': + return `${SHARE_HOST}?shareToken=${encodeURI(token)}#share/dashboard` + case 'widget': + return `${SHARE_HOST}?shareToken=${encodeURI(token)}#share/dashboard` + case 'display': + return `${SHARE_HOST}?shareToken=${encodeURI(token)}#share/display` } - setShareType(val) - }, - [setShareType, mode, roles, type, viewerIds] - ) + } else { + return '' + } + }, [mode, type, shareToken, passwordShareToken, authorizedShareToken]) return ( = (props) => { destroyOnClose >
- -
{Content}
+ +
) diff --git a/webapp/app/components/SharePanel/types.ts b/webapp/app/components/SharePanel/types.ts index fd999ab72..34872390d 100644 --- a/webapp/app/components/SharePanel/types.ts +++ b/webapp/app/components/SharePanel/types.ts @@ -2,7 +2,7 @@ import { tuple } from 'utils/util' export const shareVizsType = tuple('dashboard', 'display', 'widget') export const mode = tuple('NORMAL', 'AUTH', 'PASSWORD', '') export const permission = tuple('SHARER', 'VIEWER') -export const copyType = tuple('link', 'linkPwd') +export const copyType = tuple('link', 'all') export type TShareVizsType = typeof shareVizsType[number] export type TPermission = typeof permission[number] @@ -20,17 +20,18 @@ export interface ISharePanel { loading: boolean } -export interface IGetTokenParams { - id: number, - itemId?: number, - mode?: Tmode, - permission?: TPermission, - roles?: number[], - viewerIds?: number[] +export interface IShareTokenParams { + id: number + mode: Tmode + expired: string + itemId?: number + permission?: TPermission + roles?: number[] + viewers?: number[] } export interface ICtrl { mode: Tmode - setSType: TsType + onModeChange: TsType } export interface IContent { @@ -42,7 +43,6 @@ export interface IContent { authorizedShareToken: string } - export interface IRegularContent extends IContent { token: string vizType: TShareVizsType @@ -59,11 +59,3 @@ export interface ISignalContent extends IContent { authUser: string vizType: TShareVizsType } - - - - - - - - diff --git a/webapp/app/containers/ControlPanel/Global.tsx b/webapp/app/containers/ControlPanel/Global.tsx index 43888233f..fe4e7535c 100644 --- a/webapp/app/containers/ControlPanel/Global.tsx +++ b/webapp/app/containers/ControlPanel/Global.tsx @@ -35,10 +35,12 @@ import { ControlPanelLayoutTypes } from 'app/components/Control/constants' import { IDashboard, IDashboardItem } from '../Dashboard/types' +import { IFormedViews, IShareFormedViews } from '../View/types' interface IGlobalControlPanelBaseProps { currentDashboard: IDashboard currentItems: IDashboardItem[] + formedViews: IFormedViews | IShareFormedViews layoutType: ControlPanelLayoutTypes onGetOptions: OnGetControlOptions onSearch: ( @@ -86,7 +88,12 @@ class GlobalControlPanel extends Component< } private search = (formValues?: object) => { - const { currentDashboard, currentItems, onSearch, onMonitoredSearchDataAction } = this.props + const { + currentDashboard, + currentItems, + onSearch, + onMonitoredSearchDataAction + } = this.props const controls = currentDashboard.config.filters const relatedItems = formValues ? getFormValuesRelatedItems(controls, formValues) @@ -102,6 +109,7 @@ class GlobalControlPanel extends Component< layoutType, currentDashboard, currentItems, + formedViews, selectOptions, globalControlPanelFormValues, onGetOptions, @@ -121,6 +129,7 @@ class GlobalControlPanel extends Component< currentDashboard && ( { public render() { const { + formedViews, itemId, widget, layoutType, @@ -83,6 +85,7 @@ class LocalControlPanel extends PureComponent { !!widget.config.controls.length && ( { const dispatch = useDispatch() const sharePanelStates = useSelector(makeSelectSharePanel()) - const dashboardShareToken = useSelector(makeSelectCurrentDashboardShareToken()) - const dashboardAuthorizedShareToken = useSelector(makeSelectCurrentDashboardAuthorizedShareToken()) - const dashboardPasswordShareToken = useSelector(makeSelectCurrentDashboardPasswordShareToken()) - const dashboardPasswordSharePassword = useSelector(makeSelectCurrentDashboardPasswordSharePassword()) - const dashboardShareLoading = useSelector(makeSelectCurrentDashboardShareLoading()) + const dashboardShareToken = useSelector( + makeSelectCurrentDashboardShareToken() + ) + const dashboardAuthorizedShareToken = useSelector( + makeSelectCurrentDashboardAuthorizedShareToken() + ) + const dashboardPasswordShareToken = useSelector( + makeSelectCurrentDashboardPasswordShareToken() + ) + const dashboardPasswordSharePassword = useSelector( + makeSelectCurrentDashboardPasswordSharePassword() + ) + const dashboardShareLoading = useSelector( + makeSelectCurrentDashboardShareLoading() + ) const currentItemsInfo = useSelector(makeSelectCurrentItemsInfo()) const projectRoles = useSelector(makeSelectProjectRoles()) - const organizationMembers = useSelector(makeSelectCurrentOrganizationMembers()) + const organizationMembers = useSelector( + makeSelectCurrentOrganizationMembers() + ) - const onLoadDashboardShareLink = useCallback(({id, mode, permission, roles, viewerIds}) => { - dispatch(loadDashboardShareLink({id, mode, permission, roles, viewerIds})) - }, []) + const onLoadDashboardShareLink = useCallback( + ({ id, mode, expired, permission, roles, viewers }) => { + dispatch( + loadDashboardShareLink({ + id, + mode, + expired, + permission, + roles, + viewers + }) + ) + }, + [] + ) - const onLoadWidgetShareLink = useCallback(({id, itemId, mode, permission, roles, viewerIds}) => { - dispatch(loadWidgetShareLink({id, itemId, mode, permission, roles, viewerIds})) - }, []) + const onLoadWidgetShareLink = useCallback( + ({ id, itemId, mode, expired, permission, roles, viewers }) => { + dispatch( + loadWidgetShareLink({ + id, + itemId, + mode, + expired, + permission, + roles, + viewers + }) + ) + }, + [] + ) const onCloseSharePanel = useCallback(() => { dispatch(closeSharePanel()) @@ -70,23 +103,23 @@ const SharePanel: FC = () => { const { type, itemId } = sharePanelStates let shareToken = '' let authorizedShareToken = '' - let pwdToken = '' - let pwd = '' + let passwordShareToken = '' + let password = '' let shareLoading = false switch (type) { case 'dashboard': shareToken = dashboardShareToken authorizedShareToken = dashboardAuthorizedShareToken - pwd = dashboardPasswordSharePassword - pwdToken = dashboardPasswordShareToken + password = dashboardPasswordSharePassword + passwordShareToken = dashboardPasswordShareToken shareLoading = dashboardShareLoading break case 'widget': const itemInfo = currentItemsInfo[itemId] shareToken = itemInfo.shareToken authorizedShareToken = itemInfo.authorizedShareToken - pwd = itemInfo.pwd - pwdToken = itemInfo.pwdToken + password = itemInfo.password + passwordShareToken = itemInfo.passwordShareToken shareLoading = itemInfo.shareLoading break } @@ -95,10 +128,10 @@ const SharePanel: FC = () => { isTrigger?: boolean datasource: any @@ -240,7 +241,7 @@ export class DashboardItem extends React.PureComponent { + private syncData = () => { const { itemId, onLoadData, @@ -470,6 +471,7 @@ export class DashboardItem extends React.PureComponent
- {!loading && } + {!loading && } {widgetButton} @@ -705,6 +707,7 @@ export class DashboardItem extends React.PureComponent
{ const relatedWidget = widgets.find((w) => w.id === item.widgetId) - const initialItemInfo = getInitialItemInfo(relatedWidget) + const initialItemInfo = getInitialItemInfo(relatedWidget, formedViews) const drillpathSetting = item.config && item.config.length ? JSON.parse(item.config) : void 0 @@ -124,7 +126,10 @@ const dashboardReducer = ( const relatedWidget = action.payload.widgets.find( (w) => w.id === item.widgetId ) - draft.currentItemsInfo[item.id] = getInitialItemInfo(relatedWidget) + draft.currentItemsInfo[item.id] = getInitialItemInfo( + relatedWidget, + action.payload.formedViews + ) }) break @@ -290,10 +295,8 @@ const dashboardReducer = ( break case ActionTypes.LOAD_DASHBOARD_PASSWORD_SHARE_LINK_SUCCESS: - draft.currentDashboardPasswordShareToken = - action.payload.pwdToken - draft.currentDashboardPasswordSharePassword = - action.payload.pwd + draft.currentDashboardPasswordShareToken = action.payload.passwordShareToken + draft.currentDashboardPasswordSharePassword = action.payload.password draft.currentDashboardShareLoading = false break @@ -304,8 +307,9 @@ const dashboardReducer = ( case ActionTypes.LOAD_WIDGET_SHARE_LINK: draft.currentItemsInfo[action.payload.params.itemId].shareLoading = true if (action.payload.params.mode === 'AUTH') { - draft.currentItemsInfo[action.payload.params.itemId].authorizedShareToken = - '' + draft.currentItemsInfo[ + action.payload.params.itemId + ].authorizedShareToken = '' } break @@ -317,14 +321,15 @@ const dashboardReducer = ( case ActionTypes.LOAD_WIDGET_AUTHORIZED_SHARE_LINK_SUCCESS: targetItemInfo = draft.currentItemsInfo[action.payload.itemId] - targetItemInfo.authorizedShareToken = action.payload.authorizedShareToken + targetItemInfo.authorizedShareToken = + action.payload.authorizedShareToken targetItemInfo.shareLoading = false break case ActionTypes.LOAD_WIDGET_PASSWORD_SHARE_LINK_SUCCESS: targetItemInfo = draft.currentItemsInfo[action.payload.itemId] - targetItemInfo.pwdToken = action.payload.pwdToken - targetItemInfo.pwd = action.payload.pwd + targetItemInfo.passwordShareToken = action.payload.passwordShareToken + targetItemInfo.password = action.payload.password targetItemInfo.shareLoading = false break diff --git a/webapp/app/containers/Dashboard/sagas.ts b/webapp/app/containers/Dashboard/sagas.ts index 40f7b9504..880a6d18a 100644 --- a/webapp/app/containers/Dashboard/sagas.ts +++ b/webapp/app/containers/Dashboard/sagas.ts @@ -41,6 +41,7 @@ import { makeSelectGlobalControlPanelFormValues, makeSelectLocalControlPanelFormValues } from 'containers/ControlPanel/selectors' +import { makeSelectFormedViews } from '../View/selectors' import { getRequestParams, getRequestBody, @@ -62,6 +63,7 @@ import { ILocalControlConditions } from 'app/components/Control/types' import { IWidgetFormed } from '../Widget/types' +import { IFormedViews } from '../View/types' import { DownloadTypes } from '../App/constants' import { dashboardConfigMigrationRecorder } from 'app/utils/migrationRecorders' import { ControlPanelTypes } from 'app/components/Control/constants' @@ -86,7 +88,7 @@ export function* getDashboardDetail(action: DashboardActionType) { ) const { - widgets: items, + relations: items, views, config, ...rest @@ -101,7 +103,19 @@ export function* getDashboardDetail(action: DashboardActionType) { operationWidgetProps.widgetIntoPool(widgets) - yield put(dashboardDetailLoaded(dashboard, items, widgets, views)) + const formedViews: IFormedViews = views.reduce( + (obj, view) => { + obj[view.id] = { + ...view, + model: JSON.parse(view.model || '{}'), + variable: JSON.parse(view.variable || '[]') + } + return obj + }, + {} + ) + + yield put(dashboardDetailLoaded(dashboard, items, widgets, formedViews)) } catch (err) { yield put(loadDashboardDetailFail()) errorHandler(err) @@ -122,7 +136,8 @@ export function* addDashboardItems(action: DashboardActionType) { data: items }) const widgets: IWidgetFormed[] = yield select(makeSelectWidgets()) - yield put(dashboardItemsAdded(result.payload, widgets)) + const formedViews: IFormedViews = yield select(makeSelectFormedViews()) + yield put(dashboardItemsAdded(result.payload, widgets, formedViews)) resolve(result.payload) } catch (err) { yield put(addDashboardItemsFail()) @@ -273,6 +288,7 @@ export function* getBatchDataWithControlValues(action: DashboardActionType) { return } const { type, itemId, formValues, cancelTokenSource } = action.payload + const formedViews: IFormedViews = yield select(makeSelectFormedViews()) if (type === ControlPanelTypes.Global) { const currentDashboard: IDashboard = yield select( @@ -284,6 +300,7 @@ export function* getBatchDataWithControlValues(action: DashboardActionType) { const globalControlConditionsByItem = getCurrentControlValues( type, currentDashboard.config.filters, + formedViews, globalControlFormValues, formValues ) @@ -315,6 +332,7 @@ export function* getBatchDataWithControlValues(action: DashboardActionType) { const localControlConditions = getCurrentControlValues( type, relatedWidget.config.controls, + formedViews, localControlFormValues, formValues ) @@ -333,12 +351,14 @@ function getDownloadInfo( itemId: number, itemInfo: IDashboardItemInfo, relatedWidget: IWidgetFormed, + formedViews: IFormedViews, localControlFormValues: object, globalControlConditions: IGlobalControlConditions ): IDataDownloadStatistic { const localControlConditions = getCurrentControlValues( ControlPanelTypes.Local, relatedWidget.config.controls, + formedViews, localControlFormValues ) const requestParams = getRequestParams( @@ -373,12 +393,14 @@ export function* initiateDownloadTask(action: DashboardActionType) { const currentDashboard: IDashboard = yield select( makeSelectCurrentDashboard() ) + const formedViews: IFormedViews = yield select(makeSelectFormedViews()) const globalControlFormValues = yield select( makeSelectGlobalControlPanelFormValues() ) const globalControlConditionsByItem: IGlobalControlConditionsByItem = getCurrentControlValues( ControlPanelTypes.Global, currentDashboard.config.filters, + formedViews, globalControlFormValues ) @@ -406,6 +428,7 @@ export function* initiateDownloadTask(action: DashboardActionType) { itemId, itemInfo, relatedWidget, + formedViews, localControlFormValues, globalControlConditions ) @@ -429,6 +452,7 @@ export function* initiateDownloadTask(action: DashboardActionType) { itemId, itemInfo, relatedWidget, + formedViews, localControlFormValues, globalControlConditionsByItem[itemId] ) @@ -460,18 +484,16 @@ export function* getDashboardShareLink(action: DashboardActionType) { loadDashboardShareLinkFail } = DashboardActions - const {id, mode, permission, roles, viewerIds} = action.payload.params + const {id, mode, permission, expired, roles, viewers} = action.payload.params let requestData = null switch(mode) { case 'AUTH': - requestData = { mode, permission, roles, viewers: viewerIds } + requestData = { mode, expired, permission, roles, viewers } break case 'PASSWORD': - requestData = { mode } - break case 'NORMAL': - requestData = { mode } + requestData = { mode, expired } break default: break @@ -515,18 +537,16 @@ export function* getWidgetShareLink (action: DashboardActionType) { widgetShareLinkLoaded, loadWidgetShareLinkFail } = DashboardActions - const {id, itemId, mode, permission, roles, viewerIds} = action.payload.params + const {id, itemId, mode, expired, permission, roles, viewers} = action.payload.params let requestData = null switch(mode) { case 'AUTH': - requestData = { mode, permission, roles, viewers: viewerIds } + requestData = { mode, expired, permission, roles, viewers } break case 'PASSWORD': - requestData = { mode } - break case 'NORMAL': - requestData = { mode } + requestData = { mode, expired } break default: break diff --git a/webapp/app/containers/Dashboard/types.ts b/webapp/app/containers/Dashboard/types.ts index 65847afa5..489a183cd 100644 --- a/webapp/app/containers/Dashboard/types.ts +++ b/webapp/app/containers/Dashboard/types.ts @@ -40,7 +40,7 @@ export interface IDashboardConfig { } export interface IDashboardDetailRaw extends IDashboardRaw { - widgets: IDashboardItem[] + relations: IDashboardItem[] views: IView[] } @@ -63,8 +63,8 @@ export interface IDashboardItemInfo { loading: boolean queryConditions: IQueryConditions shareToken: string - pwdToken?: string - pwd?: string + passwordShareToken?: string + password?: string authorizedShareToken: string shareLoading: boolean downloadCsvLoading: boolean diff --git a/webapp/app/containers/Dashboard/util.ts b/webapp/app/containers/Dashboard/util.ts index accb7abb3..4546a26a9 100644 --- a/webapp/app/containers/Dashboard/util.ts +++ b/webapp/app/containers/Dashboard/util.ts @@ -21,9 +21,9 @@ import omit from 'lodash/omit' import { getPreciseDefaultValue, - getModelValue, - getVariableValue, - getCustomOptionVariableValues + getFilterParams, + getVariableParams, + getCustomOptionVariableParams } from 'app/components/Control/util' import { FieldSortTypes } from '../Widget/components/Config/Sort' import { decodeMetricName } from '../Widget/components/util' @@ -39,14 +39,17 @@ import { IControl, IGlobalControlConditionsByItem, ILocalControlConditions, - IControlRelatedField, IGlobalControlConditions } from 'app/components/Control/types' import { ControlPanelTypes, ControlFieldTypes } from 'app/components/Control/constants' -import { IViewQueryResponse } from '../View/types' +import { + IFormedViews, + IShareFormedViews, + IViewQueryResponse +} from '../View/types' import { IPaginationParams } from '../Widget/components/Widget' export function getInitialPagination(widget: IWidgetFormed): IPaginationParams { @@ -96,7 +99,10 @@ export function getUpdatedPagination( } } -export function getInitialItemInfo(widget: IWidgetFormed): IDashboardItemInfo { +export function getInitialItemInfo( + widget: IWidgetFormed, + formedViews: IFormedViews +): IDashboardItemInfo { return { datasource: { columns: [], @@ -112,7 +118,7 @@ export function getInitialItemInfo(widget: IWidgetFormed): IDashboardItemInfo { linkageVariables: [], globalVariables: [], drillpathInstance: [], - ...getLocalControlInitialValues(widget.config.controls), + ...getLocalControlInitialValues(widget.config.controls, formedViews), ...getInitialPaginationAndNativeQuery(widget) }, shareToken: '', @@ -132,7 +138,8 @@ interface IGlobalControlInitialValues { } export function getGlobalControlInitialValues( - controls: IControl[] + controls: IControl[], + formedViews: IFormedViews | IShareFormedViews ): IGlobalControlInitialValues { const initialValues: IGlobalControlInitialValues = {} controls.forEach((control: IControl) => { @@ -141,7 +148,12 @@ export function getGlobalControlInitialValues( if (defaultValue) { Object.entries(relatedItems).forEach(([itemId, config]) => { Object.entries(relatedViews).forEach(([viewId, relatedView]) => { - if (config.checked && config.viewId === Number(viewId)) { + if ( + config.checked && + config.viewId === Number(viewId) && + formedViews[viewId] + ) { + const { model, variable } = formedViews[viewId] if (!initialValues[itemId]) { initialValues[itemId] = { globalFilters: [], @@ -149,29 +161,32 @@ export function getGlobalControlInitialValues( } } if (optionWithVariable) { - const filterValue = getCustomOptionVariableValues( + const filterValue = getCustomOptionVariableParams( control, Number(viewId), - defaultValue + defaultValue, + variable ) initialValues[itemId].globalVariables = initialValues[ itemId ].globalVariables.concat(filterValue) } else { if (relatedView.fieldType === ControlFieldTypes.Column) { - const filterValue = getModelValue( + const filterValue = getFilterParams( control, - relatedView.fields as IControlRelatedField, - defaultValue + relatedView.fields, + defaultValue, + model ) initialValues[itemId].globalFilters = initialValues[ itemId ].globalFilters.concat(filterValue) } else { - const filterValue = getVariableValue( + const filterValue = getVariableParams( control, relatedView.fields, - defaultValue + defaultValue, + variable ) initialValues[itemId].globalVariables = initialValues[ itemId @@ -187,7 +202,8 @@ export function getGlobalControlInitialValues( } export function getLocalControlInitialValues( - controls: IControl[] + controls: IControl[], + formedViews: IFormedViews | IShareFormedViews ): ILocalControlConditions { const initialValues: ILocalControlConditions = { tempFilters: [], // @TODO combine widget static filters with local filters @@ -198,32 +214,40 @@ export function getLocalControlInitialValues( const defaultValue = getPreciseDefaultValue(control) if (defaultValue) { Object.entries(relatedViews).forEach(([viewId, relatedView]) => { - if (optionWithVariable) { - const filterValue = getCustomOptionVariableValues( - control, - Number(viewId), - defaultValue - ) - initialValues.variables = initialValues.variables.concat(filterValue) - } else { - if (relatedView.fieldType === ControlFieldTypes.Column) { - const filterValue = getModelValue( + if (formedViews[viewId]) { + const { model, variable } = formedViews[viewId] + if (optionWithVariable) { + const filterValue = getCustomOptionVariableParams( control, - relatedView.fields as IControlRelatedField, - defaultValue - ) - initialValues.tempFilters = initialValues.tempFilters.concat( - filterValue - ) - } else { - const filterValue = getVariableValue( - control, - relatedView.fields, - defaultValue + Number(viewId), + defaultValue, + variable ) initialValues.variables = initialValues.variables.concat( filterValue ) + } else { + if (relatedView.fieldType === ControlFieldTypes.Column) { + const filterValue = getFilterParams( + control, + relatedView.fields, + defaultValue, + model + ) + initialValues.tempFilters = initialValues.tempFilters.concat( + filterValue + ) + } else { + const filterValue = getVariableParams( + control, + relatedView.fields, + defaultValue, + variable + ) + initialValues.variables = initialValues.variables.concat( + filterValue + ) + } } } }) @@ -441,6 +465,7 @@ export function getFormValuesRelatedItems( export function getCurrentControlValues( type: ControlPanelTypes, controls: IControl[], + formedViews: IFormedViews | IShareFormedViews, allFormValues: object, changedFormValues?: object ): IGlobalControlConditionsByItem | ILocalControlConditions { @@ -465,8 +490,13 @@ export function getCurrentControlValues( const { optionWithVariable, relatedViews, relatedItems } = control const relatedItem = relatedItems[itemId] - if (relatedItem && relatedItem.checked) { + if ( + relatedItem && + relatedItem.checked && + formedViews[relatedItem.viewId] + ) { const relatedView = relatedViews[relatedItem.viewId] + const { model, variable } = formedViews[relatedItem.viewId] if (!conditionsByItem[itemId]) { conditionsByItem[itemId] = { globalVariables: [], @@ -474,29 +504,32 @@ export function getCurrentControlValues( } } if (optionWithVariable) { - const controlVariables = getCustomOptionVariableValues( + const controlVariables = getCustomOptionVariableParams( control, relatedItem.viewId, - value + value, + variable ) conditionsByItem[itemId].globalVariables = conditionsByItem[ itemId ].globalVariables.concat(controlVariables) } else { if (relatedView.fieldType === ControlFieldTypes.Column) { - const controlFilters = getModelValue( + const controlFilters = getFilterParams( control, - relatedView.fields as IControlRelatedField, - value + relatedView.fields, + value, + model ) conditionsByItem[itemId].globalFilters = conditionsByItem[ itemId ].globalFilters.concat(controlFilters) } else { - const controlVariables = getVariableValue( + const controlVariables = getVariableParams( control, relatedView.fields, - value + value, + variable ) conditionsByItem[itemId].globalVariables = conditionsByItem[ itemId @@ -520,30 +553,38 @@ export function getCurrentControlValues( if (control) { const [viewId, relatedView] = Object.entries(control.relatedViews)[0] - if (control.optionWithVariable) { - const controlVariables = getCustomOptionVariableValues( - control, - Number(viewId), - value - ) - conditions.variables = conditions.variables.concat(controlVariables) - } else { - if (relatedView.fieldType === ControlFieldTypes.Column) { - const controlFilters = getModelValue( - control, - relatedView.fields as IControlRelatedField, - value - ) - conditions.tempFilters = conditions.tempFilters.concat( - controlFilters - ) - } else { - const controlVariables = getVariableValue( + if (formedViews[viewId]) { + const { model, variable } = formedViews[viewId] + if (control.optionWithVariable) { + const controlVariables = getCustomOptionVariableParams( control, - relatedView.fields, - value + Number(viewId), + value, + variable ) conditions.variables = conditions.variables.concat(controlVariables) + } else { + if (relatedView.fieldType === ControlFieldTypes.Column) { + const controlFilters = getFilterParams( + control, + relatedView.fields, + value, + model + ) + conditions.tempFilters = conditions.tempFilters.concat( + controlFilters + ) + } else { + const controlVariables = getVariableParams( + control, + relatedView.fields, + value, + variable + ) + conditions.variables = conditions.variables.concat( + controlVariables + ) + } } } } diff --git a/webapp/app/containers/Display/Editor/SharePanel.tsx b/webapp/app/containers/Display/Editor/SharePanel.tsx index 387652e95..5ee169df1 100644 --- a/webapp/app/containers/Display/Editor/SharePanel.tsx +++ b/webapp/app/containers/Display/Editor/SharePanel.tsx @@ -30,28 +30,43 @@ import { makeSelectCurrentDisplayPasswordShareToken, makeSelectCurrentDisplayPasswordSharePassword } from '../selectors' -import { - makeSelectCurrentOrganizationMembers -} from 'containers/Organizations/selectors' -import { - makeSelectProjectRoles -} from 'containers/Projects/selectors' +import { makeSelectCurrentOrganizationMembers } from 'containers/Organizations/selectors' +import { makeSelectProjectRoles } from 'containers/Projects/selectors' const SharePanel: FC = () => { const dispatch = useDispatch() const sharePanelStates = useSelector(makeSelectSharePanel()) const shareToken = useSelector(makeSelectCurrentDisplayShareToken()) - const authorizedShareToken = useSelector(makeSelectCurrentDisplayAuthorizedShareToken()) + const authorizedShareToken = useSelector( + makeSelectCurrentDisplayAuthorizedShareToken() + ) const { shareToken: shareLoading } = useSelector(makeSelectDisplayLoading()) const projectRoles = useSelector(makeSelectProjectRoles()) - const organizationMembers = useSelector(makeSelectCurrentOrganizationMembers()) - const displayPasswordShareToken = useSelector(makeSelectCurrentDisplayPasswordShareToken()) - const displayPasswordSharePassword = useSelector(makeSelectCurrentDisplayPasswordSharePassword()) - + const organizationMembers = useSelector( + makeSelectCurrentOrganizationMembers() + ) + const displayPasswordShareToken = useSelector( + makeSelectCurrentDisplayPasswordShareToken() + ) + const displayPasswordSharePassword = useSelector( + makeSelectCurrentDisplayPasswordSharePassword() + ) - const onLoadDisplayShareLink = useCallback(({id, mode, permission, roles, viewerIds}) => { - dispatch(DisplayActions.loadDisplayShareLink({id, mode, permission, roles, viewerIds})) - }, []) + const onLoadDisplayShareLink = useCallback( + ({ id, mode, expired, permission, roles, viewers }) => { + dispatch( + DisplayActions.loadDisplayShareLink({ + id, + mode, + expired, + permission, + roles, + viewers + }) + ) + }, + [] + ) const onCloseSharePanel = useCallback(() => { dispatch(DisplayActions.closeSharePanel()) @@ -61,10 +76,10 @@ const SharePanel: FC = () => { { + obj[view.id] = { + ...view, + model: JSON.parse(view.model || '{}'), + variable: JSON.parse(view.variable || '[]') + } + return obj + }, + {} + ) + + yield put(slideDetailLoaded(slideId, items, widgets, formedViews)) } catch (err) { yield put(loadSlideDetailFail(err)) } @@ -548,7 +561,7 @@ export function* getDisplayShareLink (action: DisplayActionType) { return } - const { id, mode, permission, roles, viewerIds } = action.payload.params + const { id, mode, expired, permission, roles, viewers } = action.payload.params const { displayAuthorizedShareLinkLoaded, displayShareLinkLoaded, @@ -560,13 +573,11 @@ export function* getDisplayShareLink (action: DisplayActionType) { switch (mode) { case 'AUTH': - requestData = { mode, permission, roles, viewers: viewerIds } + requestData = { mode, expired, permission, roles, viewers } break case 'PASSWORD': - requestData = { mode } - break case 'NORMAL': - requestData = { mode } + requestData = { mode, expired } break default: break diff --git a/webapp/app/containers/View/reducer.ts b/webapp/app/containers/View/reducer.ts index 2c4a5a875..976b3d353 100644 --- a/webapp/app/containers/View/reducer.ts +++ b/webapp/app/containers/View/reducer.ts @@ -131,8 +131,16 @@ const viewReducer = ( draft.formedViews = action.payload.viewIds.reduce((acc, id) => { if (!acc[id]) { acc[id] = { + id, + name: '', + description: '', + sql: '', + config: '', + sourceId: 0, + projectId: 0, model: {}, - variable: [] + variable: [], + roles: [] } } return acc @@ -295,20 +303,9 @@ const viewReducer = ( break case DashboardActionTypes.LOAD_DASHBOARD_DETAIL_SUCCESS: case DisplayActionTypes.LOAD_SLIDE_DETAIL_SUCCESS: - const updatedViews: IFormedViews = (action.payload.views || []).reduce( - (obj, view) => { - obj[view.id] = { - ...view, - model: JSON.parse(view.model || '{}'), - variable: JSON.parse(view.variable || '[]') - } - return obj - }, - {} - ) draft.formedViews = { ...draft.formedViews, - ...updatedViews + ...action.payload.formedViews } break case ActionTypes.LOAD_VIEW_DATA_FROM_VIZ_ITEM: @@ -328,5 +325,5 @@ const viewReducer = ( } }) -export { initialState as viewInitialState} +export { initialState as viewInitialState } export default viewReducer diff --git a/webapp/app/containers/View/types.ts b/webapp/app/containers/View/types.ts index 8cf66bb26..bbb48b52c 100644 --- a/webapp/app/containers/View/types.ts +++ b/webapp/app/containers/View/types.ts @@ -157,7 +157,13 @@ export interface IViewInfo { } export interface IFormedViews { - [viewId: number]: IFormedView + [viewId: string]: IFormedView +} + +export interface IShareFormedViews { + [viewId: string]: Pick & { + dataToken: string + } } export type IDacChannel = string diff --git a/webapp/app/containers/Widget/components/Workbench/Dropbox/index.tsx b/webapp/app/containers/Widget/components/Workbench/Dropbox/index.tsx index 518a3b16f..5401339ca 100644 --- a/webapp/app/containers/Widget/components/Workbench/Dropbox/index.tsx +++ b/webapp/app/containers/Widget/components/Workbench/Dropbox/index.tsx @@ -12,7 +12,7 @@ import { IFieldFormatConfig } from '../../Config/Format' import { IFieldSortConfig, FieldSortTypes } from '../../Config/Sort' import { decodeMetricName } from '../../util' import { Popover, Icon } from 'antd' -import { IFilters } from 'app/components/Control/types' +import { IFilter } from 'app/components/Control/types' const styles = require('../Workbench.less') @@ -49,7 +49,7 @@ export interface IDataParamConfig { [key: string]: string }, sql?: string - sqlModel?: IFilters[] + sqlModel?: IFilter[] filterSource?: any field?: { alias: string, diff --git a/webapp/app/containers/Widget/components/Workbench/FilterSettingForm.tsx b/webapp/app/containers/Widget/components/Workbench/FilterSettingForm.tsx index 7cba80bcd..f49db45ab 100644 --- a/webapp/app/containers/Widget/components/Workbench/FilterSettingForm.tsx +++ b/webapp/app/containers/Widget/components/Workbench/FilterSettingForm.tsx @@ -7,7 +7,7 @@ import { DEFAULT_DATETIME_FORMAT, DEFAULT_DATE_FORMAT } from 'app/globalConstant import { decodeMetricName } from '../util' import { uuid } from 'utils/util' import { Transfer, Radio, Button, DatePicker } from 'antd' -import { IFilters } from 'app/components/Control/types' +import { IFilter } from 'app/components/Control/types' const RadioGroup = Radio.Group const RadioButton = Radio.Button const RangePicker = DatePicker.RangePicker @@ -238,7 +238,7 @@ export class FilterSettingForm extends PureComponent=', type: 'filter', @@ -301,7 +301,7 @@ export class FilterSettingForm extends PureComponent `'${key}'`).join(',') const sqlModel = [] - const filterItem: IFilters = { + const filterItem: IFilter = { name, type: 'filter', value: target.map((key) => `'${key}'`), diff --git a/webapp/app/containers/Widget/types.ts b/webapp/app/containers/Widget/types.ts index 61a050c33..4ce0c8d4e 100644 --- a/webapp/app/containers/Widget/types.ts +++ b/webapp/app/containers/Widget/types.ts @@ -37,7 +37,6 @@ export interface IWidgetRaw extends IWidgetBase { export interface IWidgetFormed extends IWidgetBase { config: IWidgetConfig dataToken?: string - password?: string } export interface IWidgetState { diff --git a/webapp/app/globalConstants.ts b/webapp/app/globalConstants.ts index 29688107d..1b8b0e67f 100644 --- a/webapp/app/globalConstants.ts +++ b/webapp/app/globalConstants.ts @@ -20,6 +20,7 @@ export const CLIENT_VERSION = '0.3-beta.9' export const API_HOST = '/api/v3' +export const SHARE_HOST = `${location.origin}/share.html` const defaultEchartsTheme = require('assets/json/echartsThemes/default.project.json') export const DEFAULT_ECHARTS_THEME = defaultEchartsTheme.theme diff --git a/webapp/app/utils/migrationRecorders/control.ts b/webapp/app/utils/migrationRecorders/control.ts index c51ec7145..732daeedc 100644 --- a/webapp/app/utils/migrationRecorders/control.ts +++ b/webapp/app/utils/migrationRecorders/control.ts @@ -38,8 +38,8 @@ import { RelativeDateValueType } from 'app/components/RelativeDatePicker/constants' -function beta5(control: IGlobalControl): IGlobalControl -function beta5(control: ILocalControl): ILocalControl +function beta5(control: ILegacyGlobalControl): ILegacyGlobalControl +function beta5(control: ILegacyLocalControl): ILegacyLocalControl function beta5(control: IControl): IControl function beta5(control) { /* IGlobalControl & ILocalControl: @@ -60,7 +60,10 @@ function beta5(control) { return control } -function beta9(control: IGlobalControl | ILocalControl | IControl, opts) { +function beta9( + control: ILegacyGlobalControl | ILegacyLocalControl | IControl, + opts +) { /* * IGlobalControl * & --> IControl @@ -90,9 +93,9 @@ function beta9(control: IGlobalControl | ILocalControl | IControl, opts) { * 19. relatedViews: change type to IControlRelatedView * IControlRelatedView: * 1. + fieldType - * 2. + fields, typeof (IControlRelatedField | IControlRelatedField[]) - * 3. name -> fields.name / fields[n].name - * 4. type -> fields.type / fields[n].type + * 2. + fields + * 3. name -> fields(element) + * 4. - type * 5. - optionsFromColumn(move each relatedViews[first-view-id].optionsFromColumn -> autoGetOptionsFromRelatedViews) * 6. - column(move each relatedViews[first-view-id].column -> valueField) * 7. structure change @@ -101,7 +104,7 @@ function beta9(control: IGlobalControl | ILocalControl | IControl, opts) { * recent * [viewId]: { * fieldType: 'column', - * fields: { name: 'foo', type: 'VARCHAR' } + * fields: ['foo'] * } * * old @@ -112,14 +115,11 @@ function beta9(control: IGlobalControl | ILocalControl | IControl, opts) { * recent * [viewId]: { * fieldType: 'variable', - * fields: [ - * { name: 'foo', type: 'date' }, - * { name: 'bar', type: 'date' } - * ] + * fields: ['foo', 'bar'] * } */ - if ((control as IGlobalControl | ILocalControl).interactionType) { - if ((control as ILocalControl).fields) { + if ((control as ILegacyGlobalControl | ILegacyLocalControl).interactionType) { + if ((control as ILegacyLocalControl).fields) { const { interactionType, customOptions, @@ -128,7 +128,7 @@ function beta9(control: IGlobalControl | ILocalControl | IControl, opts) { defaultValue, fields, ...rest - } = control as ILocalControl + } = control as ILegacyLocalControl const { relatedView, valueInfo } = beta9FieldsTransform( fields, rest.type, @@ -159,7 +159,7 @@ function beta9(control: IGlobalControl | ILocalControl | IControl, opts) { dynamicDefaultValue, defaultValue, ...rest - } = control as IGlobalControl + } = control as ILegacyGlobalControl const migratedRelatedViews = {} let valueFieldInfo @@ -216,7 +216,7 @@ function beta9FieldsTransform( return { relatedView: { fieldType: interactionType, - fields: fields.map((v) => v) + fields: fields.map(({ name }) => name) } } } else { @@ -252,7 +252,7 @@ function beta9FieldsTransform( return { relatedView: { fieldType: interactionType, - fields: { name, type } + fields: [name] }, valueInfo } @@ -395,7 +395,7 @@ function beta9DefaultValueTransform( * legacy types */ -interface IControlBase +interface ILegacyControlBase extends Omit { interactionType: ControlFieldTypes customOptions?: boolean @@ -403,13 +403,13 @@ interface IControlBase dynamicDefaultValue?: any } -interface IGlobalControl extends IControlBase { +interface ILegacyGlobalControl extends ILegacyControlBase { relatedViews: { [viewId: string]: ILegacyControlRelatedField | ILegacyControlRelatedField[] } } -interface ILocalControl extends IControlBase { +interface ILegacyLocalControl extends ILegacyControlBase { fields: ILegacyControlRelatedField | ILegacyControlRelatedField[] } diff --git a/webapp/share/containers/App/Interceptor.tsx b/webapp/share/containers/App/Interceptor.tsx index da7e30e19..5d1011e15 100644 --- a/webapp/share/containers/App/Interceptor.tsx +++ b/webapp/share/containers/App/Interceptor.tsx @@ -48,7 +48,7 @@ const Interceptor: React.FC = (props) => { const loading: boolean = useSelector(makeSelectPermissionLoading()) const loginLoading: boolean = useSelector(makeSelectLoginLoading()) - const { shareToken, type } = useMemo( + const { shareToken } = useMemo( () => querystring(window.location.search.substr(1)), [window.location.search] ) @@ -67,7 +67,6 @@ const Interceptor: React.FC = (props) => { dispatch( AppActions.getPermissions( shareToken, - type, password, () => { setAuthPwd(true) @@ -85,12 +84,12 @@ const Interceptor: React.FC = (props) => { ) ) }, - [shareToken, type] + [shareToken] ) const afterLogin = useCallback(() => { setLegitimate(true) - dispatch(AppActions.getPermissions(shareToken, type)) + dispatch(AppActions.getPermissions(shareToken)) }, [islegitimate]) const content = useMemo(() => { diff --git a/webapp/share/containers/App/actions.ts b/webapp/share/containers/App/actions.ts index 2752b5e10..4cb0407ba 100644 --- a/webapp/share/containers/App/actions.ts +++ b/webapp/share/containers/App/actions.ts @@ -68,11 +68,12 @@ export const AppActions = { } }, - interceptored(shareType) { + interceptored(shareType, vizType) { return { type: ActionTypes.INTERCEPTOR_PREFLIGHT_SUCCESS, payload: { - shareType + shareType, + vizType } } }, @@ -83,12 +84,11 @@ export const AppActions = { } }, - getPermissions(token: string, type: string, password?: string, resolve?: () => void, reject?: () => void) { + getPermissions(token: string, password?: string, resolve?: () => void, reject?: () => void) { return { type: ActionTypes.GET_PERMISSIONS, payload: { token, - type, password, resolve, reject diff --git a/webapp/share/containers/App/index.tsx b/webapp/share/containers/App/index.tsx index 97bef12af..6748fc211 100644 --- a/webapp/share/containers/App/index.tsx +++ b/webapp/share/containers/App/index.tsx @@ -48,7 +48,7 @@ export const App: React.FC = () => { useInjectSaga({ key: 'global', saga }) const shareType: Tmode = useSelector(makeSelectShareType()) - const { shareToken, type } = useMemo( + const { shareToken } = useMemo( () => querystring(window.location.search.substr(1)), [window.location.search] ) @@ -75,7 +75,7 @@ export const App: React.FC = () => { useEffect(() => { if (shareType === 'NORMAL') { - dispatch(AppActions.getPermissions(shareToken, type)) + dispatch(AppActions.getPermissions(shareToken)) } }, [shareType]) diff --git a/webapp/share/containers/App/reducer.ts b/webapp/share/containers/App/reducer.ts index ba250ff15..8770c1cf6 100644 --- a/webapp/share/containers/App/reducer.ts +++ b/webapp/share/containers/App/reducer.ts @@ -27,6 +27,7 @@ interface IState { logged: boolean, loginUser: object shareType: Tmode + vizType: 'dashboard' | 'widget' | 'display' | '' permissionLoading: boolean download: boolean } @@ -36,6 +37,7 @@ export const initialState: IState = { logged: false, loginUser: null, shareType: '', + vizType: '', permissionLoading: false, download: false } @@ -60,6 +62,7 @@ const appReducer = (state = initialState, action) => break case ActionTypes.INTERCEPTOR_PREFLIGHT_SUCCESS: draft.shareType = action.payload.shareType + draft.vizType = action.payload.vizType break case ActionTypes.GET_PERMISSIONS: draft.permissionLoading = true diff --git a/webapp/share/containers/App/sagas.ts b/webapp/share/containers/App/sagas.ts index 2e8c28ca4..e3f67ef51 100644 --- a/webapp/share/containers/App/sagas.ts +++ b/webapp/share/containers/App/sagas.ts @@ -27,13 +27,12 @@ import request from 'utils/request' import { errorHandler } from 'utils/util' import api from 'utils/api' - -export function* login (action: AppActionType) { +export function* login(action: AppActionType) { if (action.type !== ActionTypes.LOGIN) { return } const { username, password, shareToken, resolve, reject } = action.payload - const { logged, loginFail} = AppActions + const { logged, loginFail } = AppActions try { const userInfo = yield call(request, { method: 'post', @@ -49,7 +48,7 @@ export function* login (action: AppActionType) { resolve() } } catch (err) { - if(reject) { + if (reject) { return reject() } yield put(loginFail(err)) @@ -57,7 +56,7 @@ export function* login (action: AppActionType) { } } -export function * interceptor(action: AppActionType) { +export function* interceptor(action: AppActionType) { if (action.type !== ActionTypes.INTERCEPTOR_PREFLIGHT) { return } @@ -68,25 +67,26 @@ export function * interceptor(action: AppActionType) { method: 'get', url: `${api.share}/preflight/${token}` }) + const { type, vizType } = check.payload - yield put(interceptored(check?.payload?.type)) + yield put(interceptored(type, vizType)) } catch (error) { yield put(interceptorFail()) errorHandler(error) } } -export function * getPermissions(action: AppActionType) { +export function* getPermissions(action: AppActionType) { if (action.type !== ActionTypes.GET_PERMISSIONS) { return } - const { type, token, password, resolve, reject } = action.payload + const { token, password, resolve, reject } = action.payload const { getPermissionsSuccess, getPermissionsFail } = AppActions try { const check = yield call(request, { method: 'get', url: `${api.share}/permissions/${token}`, - params: {type, password} + params: { password } }) yield put(getPermissionsSuccess(check?.payload?.download)) if (resolve) { @@ -94,14 +94,14 @@ export function * getPermissions(action: AppActionType) { } } catch (error) { if (reject) { - return reject() + return reject() } yield put(getPermissionsFail()) errorHandler(error) } } -export default function* rootAppSaga () { +export default function* rootAppSaga() { yield all([ takeLatest(ActionTypes.LOGIN, login), takeEvery(ActionTypes.INTERCEPTOR_PREFLIGHT, interceptor), diff --git a/webapp/share/containers/App/selectors.ts b/webapp/share/containers/App/selectors.ts index 677a5b14f..2bcbddeb8 100644 --- a/webapp/share/containers/App/selectors.ts +++ b/webapp/share/containers/App/selectors.ts @@ -45,6 +45,13 @@ const makeSelectShareType = () => createSelector( } ) +const makeSelectVizType = () => createSelector( + selectGlobal, + (globalState) => { + return globalState.vizType + } +) + const makeSelectPermission = () => createSelector( selectGlobal, (globalState) => { @@ -69,6 +76,7 @@ export { makeSelectLogged, makeSelectLoginUser, makeSelectShareType, + makeSelectVizType, makeSelectPermission, makeSelectPermissionLoading } diff --git a/webapp/share/containers/Dashboard/FullScreenPanel.tsx b/webapp/share/containers/Dashboard/FullScreenPanel.tsx index b84e30dfd..df615f69c 100644 --- a/webapp/share/containers/Dashboard/FullScreenPanel.tsx +++ b/webapp/share/containers/Dashboard/FullScreenPanel.tsx @@ -30,7 +30,7 @@ import { IDashboardItem } from 'app/containers/Dashboard/types' import { IWidgetFormed } from 'app/containers/Widget/types' -import { IFormedView } from 'app/containers/View/types' +import { IShareFormedViews } from 'app/containers/View/types' import { OnGetControlOptions } from 'app/components/Control/types' import { ControlPanelTypes } from 'app/components/Control/constants' import { IShareDashboardItemInfo } from './types' @@ -38,9 +38,7 @@ import { IShareDashboardItemInfo } from './types' interface IWrapperBaseProps { currentDashboard: IDashboard widgets: IWidgetFormed[] - formedViews: { - [viewId: string]: Pick - } + formedViews: IShareFormedViews currentItems: IDashboardItem[] currentItemsInfo: { [itemId: string]: IShareDashboardItemInfo diff --git a/webapp/share/containers/Dashboard/actions.ts b/webapp/share/containers/Dashboard/actions.ts index 864d27654..bfb1d05bb 100644 --- a/webapp/share/containers/Dashboard/actions.ts +++ b/webapp/share/containers/Dashboard/actions.ts @@ -27,7 +27,10 @@ import { IDataRequestParams } from 'app/containers/Dashboard/types' import { IWidgetFormed } from 'app/containers/Widget/types' -import { IFormedView, IViewQueryResponse } from 'app/containers/View/types' +import { + IShareFormedViews, + IViewQueryResponse +} from 'app/containers/View/types' import { RenderType } from 'app/containers/Widget/components/Widget' import { ControlPanelTypes } from 'app/components/Control/constants' import { IDistinctValueReqeustParams } from 'app/components/Control/types' @@ -47,9 +50,7 @@ export const DashboardActions = { dashboard: IDashboard, items: IDashboardItem[], widgets: IWidgetFormed[], - formedViews: { - [viewId: string]: Pick - } + formedViews: IShareFormedViews ) { return { type: ActionTypes.LOAD_SHARE_DASHBOARD_SUCCESS, @@ -79,12 +80,7 @@ export const DashboardActions = { } }, - widgetGetted( - widget: IWidgetFormed, - formedViews: { - [viewId: string]: Pick - } - ) { + widgetGetted(widget: IWidgetFormed, formedViews: IShareFormedViews) { return { type: ActionTypes.LOAD_SHARE_WIDGET_SUCCESS, payload: { @@ -153,11 +149,16 @@ export const DashboardActions = { } }, - setIndividualDashboard(widget: IWidgetFormed, token: string) { + setIndividualDashboard( + widget: IWidgetFormed, + formedViews: IShareFormedViews, + token: string + ) { return { type: ActionTypes.SET_INDIVIDUAL_DASHBOARD, payload: { widget, + formedViews, token } } @@ -194,7 +195,6 @@ export const DashboardActions = { loadSelectOptions( controlKey: string, - dataToken: string, requestParams: { [viewId: string]: IDistinctValueReqeustParams }, itemId: number ) { @@ -202,7 +202,6 @@ export const DashboardActions = { type: ActionTypes.LOAD_SELECT_OPTIONS, payload: { controlKey, - dataToken, requestParams, itemId } diff --git a/webapp/share/containers/Dashboard/index.tsx b/webapp/share/containers/Dashboard/index.tsx index ccf25bc65..06f959800 100644 --- a/webapp/share/containers/Dashboard/index.tsx +++ b/webapp/share/containers/Dashboard/index.tsx @@ -42,7 +42,7 @@ import FullScreenPanel from './FullScreenPanel' import { Responsive, WidthProvider } from 'react-grid-layout' import { ChartTypes } from 'containers/Widget/config/chart/ChartTypes' import { - IFilters, + IFilter, IDistinctValueReqeustParams } from 'app/components/Control/types' import GlobalControlPanel from 'app/containers/ControlPanel/Global' @@ -85,7 +85,7 @@ import { makeSelectDownloadList, makeSelectShareParams } from './selectors' -import { makeSelectLoginLoading } from '../App/selectors' +import { makeSelectLoginLoading, makeSelectVizType } from '../App/selectors' import { GRID_COLS, GRID_ROW_HEIGHT, @@ -110,6 +110,7 @@ import { ControlPanelTypes } from 'app/components/Control/constants' import { IDrillDetail } from 'components/DataDrill/types' +import { IShareFormedViews } from 'app/containers/View/types' const ResponsiveReactGridLayout = WidthProvider(Responsive) @@ -119,7 +120,6 @@ type MappedDispatches = ReturnType type IDashboardProps = MappedStates & MappedDispatches interface IDashboardStates { - type: string shareToken: string modalLoading: boolean interactingStatus: { [itemId: number]: boolean } @@ -130,7 +130,6 @@ export class Share extends React.Component { constructor(props) { super(props) this.state = { - type: '', shareToken: '', modalLoading: false, @@ -147,47 +146,40 @@ export class Share extends React.Component { private shareClientId: string = getShareClientId() private downloadListPollingTimer: number - /** - * object - * { - * type: this.state.type, - * shareToken: this.state.shareToken - * } - * @param qs - */ - private loadShareContent = (qs) => { + private loadShareContent = (shareToken: string) => { const { + vizType, onLoadDashboard, onLoadWidget, onSetIndividualDashboard } = this.props - if (qs.type === 'dashboard') { - onLoadDashboard(qs.shareToken, () => null) - } else { - onLoadWidget( - qs.shareToken, - (widget) => { - onSetIndividualDashboard(widget, qs.shareToken) - }, - () => null - ) + switch (vizType) { + case 'dashboard': + onLoadDashboard(shareToken, () => null) + break + case 'widget': + onLoadWidget( + shareToken, + (widget, formedViews) => { + onSetIndividualDashboard(widget, formedViews, shareToken) + }, + () => null + ) + break } } public componentDidMount() { // urlparse - const qs = querystring(window.location.search.substr(1)) + const { shareToken, ...rest } = querystring( + window.location.search.substr(1) + ) - this.setState({ - type: qs.type, - shareToken: qs.shareToken - }) - this.loadShareContent(qs) - this.initPolling(qs.shareToken) - delete qs.type - delete qs.shareToken - this.props.onSendShareParams(qs) + this.setState({ shareToken }) + this.loadShareContent(shareToken) + this.initPolling(shareToken) + this.props.onSendShareParams(rest) window.addEventListener('resize', this.onWindowResize, false) } @@ -332,12 +324,7 @@ export class Share extends React.Component { if (useOptions) { this.props.onSetSelectOptions(controlKey, paramsOrOptions, itemId) } else { - this.props.onLoadSelectOptions( - controlKey, - this.state.shareToken, - paramsOrOptions, - itemId - ) + this.props.onLoadSelectOptions(controlKey, paramsOrOptions, itemId) } } @@ -462,6 +449,7 @@ export class Share extends React.Component { itemId={id} widget={widget} widgets={widgets} + formedViews={formedViews} view={view} isTrigger={isTrigger} datasource={datasource} @@ -549,6 +537,7 @@ export class Share extends React.Component { { } const mapStateToProps = createStructuredSelector({ + vizType: makeSelectVizType(), dashboard: makeSelectDashboard(), title: makeSelectTitle(), widgets: makeSelectWidgets(), @@ -594,7 +584,7 @@ export function mapDispatchToProps(dispatch) { dispatch(getDashboard(token, reject)), onLoadWidget: ( token: string, - resolve: (widget: IWidgetFormed) => void, + resolve: (widget: IWidgetFormed, formedViews: IShareFormedViews) => void, reject: (err) => void ) => dispatch(getWidget(token, resolve, reject)), onLoadResultset: ( @@ -611,8 +601,11 @@ export function mapDispatchToProps(dispatch) { dispatch( getBatchDataWithControlValues(type, relatedItems, formValues, itemId) ), - onSetIndividualDashboard: (widget: IWidgetFormed, token: string) => - dispatch(setIndividualDashboard(widget, token)), + onSetIndividualDashboard: ( + widget: IWidgetFormed, + formedViews: IShareFormedViews, + token: string + ) => dispatch(setIndividualDashboard(widget, formedViews, token)), onLoadWidgetCsv: ( itemId: number, requestParams: IDataRequestParams, @@ -620,11 +613,9 @@ export function mapDispatchToProps(dispatch) { ) => dispatch(loadWidgetCsv(itemId, requestParams, dataToken)), onLoadSelectOptions: ( controlKey: string, - dataToken: string, reqeustParams: { [viewId: string]: IDistinctValueReqeustParams }, itemId: number - ) => - dispatch(loadSelectOptions(controlKey, dataToken, reqeustParams, itemId)), + ) => dispatch(loadSelectOptions(controlKey, reqeustParams, itemId)), onSetSelectOptions: (controlKey: string, options: any[], itemId?: number) => dispatch(setSelectOptions(controlKey, options, itemId)), onResizeDashboardItem: (itemId: number) => diff --git a/webapp/share/containers/Dashboard/reducer.ts b/webapp/share/containers/Dashboard/reducer.ts index 200004e04..d2f863db5 100644 --- a/webapp/share/containers/Dashboard/reducer.ts +++ b/webapp/share/containers/Dashboard/reducer.ts @@ -64,7 +64,8 @@ const shareReducer = (state = initialState, action: DashboardActionType) => ) const globalControlsInitialValue = getGlobalControlInitialValues( - dashboard.config.filters + dashboard.config.filters, + formedViews ) draft.title = dashboard.name @@ -74,7 +75,10 @@ const shareReducer = (state = initialState, action: DashboardActionType) => draft.items = items draft.itemsInfo = items.reduce((info, item) => { const relatedWidget = widgets.find((w) => w.id === item.widgetId) - const initialItemInfo = getShareInitialItemInfo(relatedWidget) + const initialItemInfo = getShareInitialItemInfo( + relatedWidget, + formedViews + ) if (globalControlsInitialValue[item.id]) { const { @@ -110,7 +114,10 @@ const shareReducer = (state = initialState, action: DashboardActionType) => } ] draft.itemsInfo = { - 1: getShareInitialItemInfo(action.payload.widget) + 1: getShareInitialItemInfo( + action.payload.widget, + action.payload.formedViews + ) } break diff --git a/webapp/share/containers/Dashboard/sagas.ts b/webapp/share/containers/Dashboard/sagas.ts index 8f56202e4..c373fadf0 100644 --- a/webapp/share/containers/Dashboard/sagas.ts +++ b/webapp/share/containers/Dashboard/sagas.ts @@ -32,11 +32,10 @@ import { DashboardActions, DashboardActionType } from './actions' import { makeSelectDashboard, makeSelectItemRelatedWidget, - makeSelectItemInfo + makeSelectItemInfo, + makeSelectFormedViews } from './selectors' -import { - makeSelectShareType -} from 'share/containers/App/selectors' +import { makeSelectShareType } from 'share/containers/App/selectors' import { makeSelectGlobalControlPanelFormValues, makeSelectLocalControlPanelFormValues @@ -57,7 +56,7 @@ import { } from 'app/containers/Dashboard/util' import { IShareDashboardDetailRaw, - IShareWidgetRaw, + IShareWidgetDetailRaw, IShareDashboardItemInfo } from './types' import { @@ -66,6 +65,7 @@ import { IQueryConditions } from 'app/containers/Dashboard/types' import { IWidgetFormed } from 'app/containers/Widget/types' +import { IShareFormedViews } from 'app/containers/View/types' import { IGlobalControlConditions, IGlobalControlConditionsByItem, @@ -83,7 +83,6 @@ import { message } from 'antd' import { DownloadTypes } from 'app/containers/App/constants' import { localStorageCRUD, getPasswordUrl } from '../../util' - export function* getDashboard(action: DashboardActionType) { if (action.type !== ActionTypes.LOAD_SHARE_DASHBOARD) { return @@ -100,18 +99,19 @@ export function* getDashboard(action: DashboardActionType) { const result = yield call(request, requestUrl) const { widgets, + views, relations, config, ...rest } = result.payload as IShareDashboardDetailRaw - + const parsedConfig: IDashboardConfig = JSON.parse(config || '{}') const dashboard = { ...rest, config: dashboardConfigMigrationRecorder(parsedConfig) } const formedWidgets = widgets.map((widget) => { - const { model, variable, config, ...rest } = widget + const { config, ...rest } = widget const parsedConfig: IWidgetConfig = JSON.parse(config) return { ...rest, @@ -120,10 +120,14 @@ export function* getDashboard(action: DashboardActionType) { }) } }) - const formedViews = widgets.reduce( - (obj, widget) => ({ + const formedViews = views.reduce( + (obj, { id, model, variable, ...rest }) => ({ ...obj, - [widget.viewId]: { model: JSON.parse(widget.model) } + [id]: { + ...rest, + model: JSON.parse(model), + variable: JSON.parse(variable) + } }), {} ) @@ -146,8 +150,8 @@ export function* getWidget(action: DashboardActionType) { const requestUrl = getPasswordUrl(shareType, token, baseUrl) try { const result = yield call(request, requestUrl) - const widget: IShareWidgetRaw = result.payload - const { model, variable, config, ...rest } = widget + const { widget, views } = result.payload as IShareWidgetDetailRaw + const { config, ...rest } = widget const parsedConfig: IWidgetConfig = JSON.parse(config) const formedWidget = { ...rest, @@ -155,13 +159,21 @@ export function* getWidget(action: DashboardActionType) { viewId: widget.viewId }) } - const formedViews = { - [widget.viewId]: { model: JSON.parse(widget.model) } - } + const formedViews = views.reduce( + (obj, { id, model, variable, ...rest }) => ({ + ...obj, + [id]: { + ...rest, + model: JSON.parse(model), + variable: JSON.parse(variable) + } + }), + {} + ) yield put(widgetGetted(formedWidget, formedViews)) if (resolve) { - resolve(formedWidget) + resolve(formedWidget, formedViews) } } catch (err) { errorHandler(err) @@ -202,7 +214,7 @@ function* getData( try { const result = yield call(request, { method: 'post', - url: `${api.share}/data/${relatedWidget.dataToken}?password=${relatedWidget.password}`, + url: `${api.share}/data/${relatedWidget.dataToken}`, data: getRequestBody(requestParams) }) result.payload.resultList = result.payload.resultList || [] @@ -231,6 +243,7 @@ export function* getBatchDataWithControlValues(action: DashboardActionType) { return } const { type, itemId, formValues } = action.payload + const formedViews: IShareFormedViews = yield select(makeSelectFormedViews()) if (type === ControlPanelTypes.Global) { const currentDashboard: IDashboard = yield select(makeSelectDashboard()) @@ -240,6 +253,7 @@ export function* getBatchDataWithControlValues(action: DashboardActionType) { const globalControlConditionsByItem = getCurrentControlValues( type, currentDashboard.config.filters, + formedViews, globalControlFormValues, formValues ) @@ -264,6 +278,7 @@ export function* getBatchDataWithControlValues(action: DashboardActionType) { const localControlConditions = getCurrentControlValues( type, relatedWidget.config.controls, + formedViews, localControlFormValues, formValues ) @@ -321,12 +336,14 @@ export function* getSelectOptions(action: DashboardActionType) { } const { selectOptionsLoaded, loadSelectOptionsFail } = DashboardActions try { - const { controlKey, dataToken, requestParams, itemId } = action.payload + const { controlKey, requestParams, itemId } = action.payload + const formedViews: IShareFormedViews = yield select(makeSelectFormedViews()) const requests = Object.entries(requestParams).map(([viewId, params]) => { const { columns, filters, variables, cache, expired } = params + const { dataToken } = formedViews[viewId] return call(request, { method: 'post', - url: `${api.share}/data/${dataToken}/distinctvalue/${viewId}`, + url: `${api.share}/data/${dataToken}/distinctvalue`, data: { columns: Object.values(columns).filter((c) => !!c), filters, @@ -357,14 +374,11 @@ export function* getDownloadList(action: DashboardActionType) { const { shareClinetId, token } = action.payload const shareType = yield select(makeSelectShareType()) - const baseUrl = `${api.download}/share/page/${shareClinetId}/${token}` + const baseUrl = `${api.download}/share/page/${shareClinetId}/${token}` const requestUrl = getPasswordUrl(shareType, token, baseUrl) try { - const result = yield call( - request, - requestUrl - ) + const result = yield call(request, requestUrl) yield put(downloadListLoaded(result.payload)) } catch (err) { yield put(loadDownloadListFail(err)) @@ -395,12 +409,14 @@ export function* initiateDownloadTask(action: DashboardActionType) { const { shareClientId, itemId } = action.payload const currentDashboard: IDashboard = yield select(makeSelectDashboard()) const currentDashboardFilters = currentDashboard?.config.filters || [] + const formedViews: IShareFormedViews = yield select(makeSelectFormedViews()) const globalControlFormValues = yield select( makeSelectGlobalControlPanelFormValues() ) const globalControlConditionsByItem: IGlobalControlConditionsByItem = getCurrentControlValues( ControlPanelTypes.Global, currentDashboardFilters, + formedViews, globalControlFormValues ) const itemInfo: IShareDashboardItemInfo = yield select((state) => @@ -415,6 +431,7 @@ export function* initiateDownloadTask(action: DashboardActionType) { const localControlConditions = getCurrentControlValues( ControlPanelTypes.Local, relatedWidget.config.controls, + formedViews, localControlFormValues ) const requestParams = getRequestParams( @@ -427,11 +444,11 @@ export function* initiateDownloadTask(action: DashboardActionType) { } ) - const { dataToken, password } = relatedWidget + const { dataToken } = relatedWidget try { yield call(request, { method: 'POST', - url: `${api.download}/share/submit/${DownloadTypes.Widget}/${shareClientId}/${dataToken}?password=${password}`, + url: `${api.download}/share/submit/${DownloadTypes.Widget}/${shareClientId}/${dataToken}`, data: [ { id: relatedWidget.id, diff --git a/webapp/share/containers/Dashboard/types.ts b/webapp/share/containers/Dashboard/types.ts index 90abada80..c5d3e9bea 100644 --- a/webapp/share/containers/Dashboard/types.ts +++ b/webapp/share/containers/Dashboard/types.ts @@ -6,20 +6,24 @@ import { } from 'app/containers/Dashboard/types' import { DashboardItemStatus } from './constants' import { IWidgetRaw, IWidgetFormed } from 'app/containers/Widget/types' -import { IFormedView } from 'app/containers/View/types' +import { IShareFormedViews, IView } from 'app/containers/View/types' import { IDownloadRecord } from 'app/containers/App/types' export interface IShareWidgetRaw extends IWidgetRaw { dataToken: string - model: string - variable: string } export interface IShareDashboardDetailRaw extends IDashboardRaw { widgets: IShareWidgetRaw[] + views: IView[] relations: IDashboardItem[] } +export interface IShareWidgetDetailRaw { + widget: IShareWidgetRaw + views: IView[] +} + export interface IShareDashboardItemInfo extends Omit< IDashboardItemInfo, @@ -32,9 +36,7 @@ export interface IShareDashboardState { dashboard: IDashboard title: string widgets: IWidgetFormed[] - formedViews: { - [viewId: number]: Pick - } + formedViews: IShareFormedViews items: IDashboardItem[] itemsInfo: { [itemId: string]: IShareDashboardItemInfo diff --git a/webapp/share/containers/Dashboard/util.ts b/webapp/share/containers/Dashboard/util.ts index f251cc7a3..9be894bb6 100644 --- a/webapp/share/containers/Dashboard/util.ts +++ b/webapp/share/containers/Dashboard/util.ts @@ -27,6 +27,7 @@ import { DashboardItemStatus } from './constants' import { IControl } from 'app/components/Control/types' import { ControlDefaultValueTypes } from 'app/components/Control/constants' import { IWidgetFormed } from 'app/containers/Widget/types' +import { IShareFormedViews } from 'app/containers/View/types' export function initDefaultValuesFromShareParams( controls: IControl[], @@ -48,7 +49,8 @@ export function initDefaultValuesFromShareParams( } export function getShareInitialItemInfo( - widget: IWidgetFormed + widget: IWidgetFormed, + formedViews: IShareFormedViews ): IShareDashboardItemInfo { return { status: DashboardItemStatus.Pending, @@ -66,7 +68,7 @@ export function getShareInitialItemInfo( linkageVariables: [], globalVariables: [], drillpathInstance: [], - ...getLocalControlInitialValues(widget.config.controls), + ...getLocalControlInitialValues(widget.config.controls, formedViews), ...getInitialPaginationAndNativeQuery(widget) }, downloadCsvLoading: false, diff --git a/webapp/share/containers/Display/Reveal/Display.tsx b/webapp/share/containers/Display/Reveal/Display.tsx index b52129c40..f4ffde972 100644 --- a/webapp/share/containers/Display/Reveal/Display.tsx +++ b/webapp/share/containers/Display/Reveal/Display.tsx @@ -21,7 +21,11 @@ import React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { makeSelectSlidesCount, makeSelectWidgets } from '../selectors' +import { + makeSelectFormedViews, + makeSelectSlidesCount, + makeSelectWidgets +} from '../selectors' import { getRequestParamsByWidgetConfig } from 'containers/Viz/util' import { @@ -41,10 +45,11 @@ const Display: React.FC = (props) => { const dispatch = useDispatch() const slidesCount = useSelector(makeSelectSlidesCount()) const widgets = useSelector(makeSelectWidgets()) + const formedViews = useSelector(makeSelectFormedViews()) const layerListContextValue: LayerListContextValue = { currentDisplayWidgets: widgets, - getWidgetViewModel: (_, widgetId: number) => widgets[widgetId].model, + getWidgetViewModel: (viewId, _) => formedViews[viewId].model, getChartData: ( renderType, slideNumber, @@ -65,7 +70,6 @@ const Display: React.FC = (props) => { slideNumber, layerId, widget.dataToken, - widget.password, requestParams ) ) diff --git a/webapp/share/containers/Display/actions.ts b/webapp/share/containers/Display/actions.ts index 3e2359c22..80b8e7e90 100644 --- a/webapp/share/containers/Display/actions.ts +++ b/webapp/share/containers/Display/actions.ts @@ -33,13 +33,14 @@ export const ShareDisplayActions = { } } }, - displayLoaded(display: IDisplayFormed, slides, widgets) { + displayLoaded(display: IDisplayFormed, slides, widgets, formedViews) { return { type: ActionTypes.LOAD_SHARE_DISPLAY_SUCCESS, payload: { display, slides, - widgets + widgets, + formedViews } } }, @@ -52,7 +53,7 @@ export const ShareDisplayActions = { } }, - loadLayerData(renderType, slideNumber, layerId, dataToken, password, requestParams) { + loadLayerData(renderType, slideNumber, layerId, dataToken, requestParams) { return { type: ActionTypes.LOAD_LAYER_DATA, payload: { @@ -60,7 +61,6 @@ export const ShareDisplayActions = { slideNumber, layerId, dataToken, - password, requestParams } } diff --git a/webapp/share/containers/Display/reducer.ts b/webapp/share/containers/Display/reducer.ts index 844e96716..8f2bb1d96 100644 --- a/webapp/share/containers/Display/reducer.ts +++ b/webapp/share/containers/Display/reducer.ts @@ -30,7 +30,8 @@ export const initialState = { display: null, slidesLayers: [], slideLayersInfo: {}, - widgets: {} + widgets: {}, + formedViews: {} } const displayReducer = (state = initialState, action) => @@ -44,6 +45,7 @@ const displayReducer = (state = initialState, action) => obj[w.id] = w return obj }, {}) + draft.formedViews = action.payload.formedViews draft.slideLayersInfo = action.payload.slides.reduce( (obj, slide, idx) => { obj[idx + 1] = slide.relations.reduce((info, layer) => { diff --git a/webapp/share/containers/Display/sagas.ts b/webapp/share/containers/Display/sagas.ts index 9c9d06752..cbe5eb098 100644 --- a/webapp/share/containers/Display/sagas.ts +++ b/webapp/share/containers/Display/sagas.ts @@ -30,9 +30,10 @@ import { getPasswordUrl } from 'share/util' import { makeSelectShareType } from 'share/containers/App/selectors' -import { displayParamsMigrationRecorder } from 'app/utils/migrationRecorders' +import { displayParamsMigrationRecorder, widgetConfigMigrationRecorder } from 'app/utils/migrationRecorders' import { SecondaryGraphTypes } from 'app/containers/Display/components/Setting' import { ILayerRaw, ILayerParams } from 'app/containers/Display/components/types' +import { IWidgetConfig } from 'app/containers/Widget/components/Widget' export function* getDisplay (action: ShareDisplayActionType) { if (action.type !== ActionTypes.LOAD_SHARE_DISPLAY) { return } @@ -52,7 +53,7 @@ export function* getDisplay (action: ShareDisplayActionType) { yield put(loadDisplayFail(header.msg)) return } - const { slides, widgets, ...display } = payload + const { slides, widgets, views, ...display } = payload display.config = JSON.parse(display.config || '{}') slides.sort((s1, s2) => s1.index - s2.index).forEach((slide) => { slide.config = JSON.parse(slide.config) @@ -62,13 +63,28 @@ export function* getDisplay (action: ShareDisplayActionType) { layer.params = SecondaryGraphTypes.Label === subType ? displayParamsMigrationRecorder(parsedParams) : parsedParams }) }) - if (Array.isArray(widgets)) { - widgets.forEach((widget) => { - widget.config = JSON.parse(widget.config) - widget.model = JSON.parse(widget.model) - }) - } - yield put(displayLoaded(display, slides, widgets || [])) // @FIXME should return empty array in response + const formedWidgets = widgets.map((widget) => { + const { config, ...rest } = widget + const parsedConfig: IWidgetConfig = JSON.parse(config) + return { + ...rest, + config: widgetConfigMigrationRecorder(parsedConfig, { + viewId: widget.viewId + }) + } + }) + const formedViews = views.reduce( + (obj, { id, model, variable }) => ({ + ...obj, + [id]: { + model: JSON.parse(model), + variable: JSON.parse(variable) + } + }), + {} + ) + + yield put(displayLoaded(display, slides, formedWidgets, formedViews)) resolve(display, slides, widgets) } catch (err) { message.destroy() @@ -81,7 +97,7 @@ export function* getDisplay (action: ShareDisplayActionType) { export function* getData (action: ShareDisplayActionType) { if (action.type !== ActionTypes.LOAD_LAYER_DATA) { return } - const { renderType, slideNumber, layerId, dataToken, password, requestParams } = action.payload + const { renderType, slideNumber, layerId, dataToken, requestParams } = action.payload const { filters, tempFilters, // @TODO combine widget static filters with local filters @@ -99,7 +115,7 @@ export function* getData (action: ShareDisplayActionType) { try { const response = yield call(request, { method: 'post', - url: `${api.share}/data/${dataToken}?password=${password}`, + url: `${api.share}/data/${dataToken}`, data: { ...omit(rest, 'customOrders'), filters: filters.concat(tempFilters).concat(linkageFilters).concat(globalFilters), diff --git a/webapp/share/containers/Display/selectors.ts b/webapp/share/containers/Display/selectors.ts index 6e584cda6..4727faf51 100644 --- a/webapp/share/containers/Display/selectors.ts +++ b/webapp/share/containers/Display/selectors.ts @@ -66,6 +66,12 @@ const makeSelectWidgets = () => (shareState) => shareState.widgets ) +const makeSelectFormedViews = () => + createSelector( + selectShare, + (shareState) => shareState.formedViews + ) + const makeSelectSlideLayerContextValue = () => createSelector( selectSlidesLayers, @@ -99,6 +105,7 @@ export { makeSelectSlidesCount, makeSelectSlideLayers, makeSelectWidgets, + makeSelectFormedViews, makeSelectSlideLayerContextValue, makeSelectSlideLayersLoaded } From 4d541d9fd9fab62c1e4165cd558a2c8404e7d4d7 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Tue, 27 Oct 2020 10:38:14 +0800 Subject: [PATCH 11/18] fix(control): cascading filters logic --- webapp/app/components/Control/Panel/index.tsx | 42 ++++--------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/webapp/app/components/Control/Panel/index.tsx b/webapp/app/components/Control/Panel/index.tsx index 62cccaa87..c91e40e31 100644 --- a/webapp/app/components/Control/Panel/index.tsx +++ b/webapp/app/components/Control/Panel/index.tsx @@ -177,7 +177,7 @@ class ControlPanel extends PureComponent< // get cascading conditions Object.entries(relatedViews).forEach(([viewId, relatedView]) => { let filters = [] - let variables = [] + const variables = [] parents.forEach((parentControl) => { const parentValue = controlValues[parentControl.key] @@ -212,39 +212,15 @@ class ControlPanel extends PureComponent< cascadeRelatedFields && formedViews[cascadeRelatedViewId] ) { - const { model, variable } = formedViews[cascadeRelatedViewId] - if (parentControl.optionWithVariable) { - variables = variables.concat( - getCustomOptionVariableParams( - parentControl, - Number(cascadeRelatedViewId), - parentValue, - variable - ) + const { model } = formedViews[cascadeRelatedViewId] + filters = filters.concat( + getFilterParams( + parentControl, + cascadeRelatedFields, + parentValue, + model ) - } else { - if ( - parentRelatedView.fieldType === ControlFieldTypes.Column - ) { - filters = filters.concat( - getFilterParams( - parentControl, - cascadeRelatedFields, - parentValue, - model - ) - ) - } else { - variables = variables.concat( - getVariableParams( - parentControl, - cascadeRelatedFields, - parentValue, - variable - ) - ) - } - } + ) } } } From cfb4c34b37b4f7781eff3ad690666199becc82f3 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Tue, 27 Oct 2020 17:11:51 +0800 Subject: [PATCH 12/18] fix: type errors --- webapp/app/containers/App/sagas.ts | 34 +++++++++++----------- webapp/app/containers/Dashboard/sagas.ts | 2 +- webapp/app/containers/Profile/sagas.ts | 4 +-- webapp/app/containers/Projects/sagas.ts | 2 +- webapp/app/containers/Register/sagas.ts | 6 ++-- webapp/app/containers/Viz/sagas.ts | 2 +- webapp/share/containers/Dashboard/sagas.ts | 2 +- webapp/share/containers/Display/sagas.ts | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/webapp/app/containers/App/sagas.ts b/webapp/app/containers/App/sagas.ts index 8576930e7..bebef5cb6 100644 --- a/webapp/app/containers/App/sagas.ts +++ b/webapp/app/containers/App/sagas.ts @@ -78,7 +78,7 @@ import api from 'utils/api' import { IReduxActionStruct } from 'utils/types' import { IResetPasswordParams, IGetgetCaptchaParams } from '../FindPassword/types' -export function* getExternalAuthProviders(): IterableIterator { +export function* getExternalAuthProviders() { try { const asyncData = yield call(request, { method: 'get', @@ -92,7 +92,7 @@ export function* getExternalAuthProviders(): IterableIterator { } } -export function* getDavinciVersion(action): IterableIterator { +export function* getDavinciVersion(action) { const {resolve} = action.payload try { const version = yield call(request, { @@ -109,7 +109,7 @@ export function* getDavinciVersion(action): IterableIterator { } } -export function* tryExternalAuth(action): IterableIterator { +export function* tryExternalAuth(action) { const { resolve } = action.payload try { const asyncData = yield call(request, { @@ -124,7 +124,7 @@ export function* tryExternalAuth(action): IterableIterator { } } -export function* login(action): IterableIterator { +export function* login(action) { const { username, password, resolve } = action.payload try { @@ -146,11 +146,11 @@ export function* login(action): IterableIterator { } } -export function* externalAuthlogout(): IterableIterator { +export function* externalAuthlogout() { location.replace(`${api.externalLogout}`) } -export function* logout(): IterableIterator { +export function* logout() { try { removeToken() localStorage.removeItem('loginUser') @@ -159,7 +159,7 @@ export function* logout(): IterableIterator { } } -export function* activeUser(action): IterableIterator { +export function* activeUser(action) { const { token, resolve } = action.payload try { const asyncData = yield call(request, { @@ -188,7 +188,7 @@ export function* activeUser(action): IterableIterator { } } -export function* checkName(action): IterableIterator { +export function* checkName(action) { const { id, name, type, params, resolve, reject } = action.payload try { const asyncData = yield call(request, `${api.checkName}/${type}`, { @@ -213,7 +213,7 @@ export function* checkName(action): IterableIterator { } } -export function* checkNameUnique(action): IterableIterator { +export function* checkNameUnique(action) { const { pathname, data, resolve, reject } = action.payload try { if (!data.name) { @@ -238,7 +238,7 @@ export function* checkNameUnique(action): IterableIterator { } } -export function* updateProfile(action): IterableIterator { +export function* updateProfile(action) { const { id, name, description, department, resolve } = action.payload try { @@ -267,7 +267,7 @@ export function* updateProfile(action): IterableIterator { export function* getCaptchaForResetPassword( action: IReduxActionStruct -): IterableIterator { +) { const { type, ticket, resolve } = action.payload try { @@ -290,7 +290,7 @@ export function* getCaptchaForResetPassword( export function* resetPasswordUnlogged( action: IReduxActionStruct -): IterableIterator { +) { const { ticket, type, token, resolve, checkCode, password } = action.payload try { @@ -328,7 +328,7 @@ export function* changeUserPassword({ payload }) { } } -export function* joinOrganization(action): IterableIterator { +export function* joinOrganization(action) { const { token, resolve, reject } = action.payload try { const asyncData = yield call(request, { @@ -367,7 +367,7 @@ export function* joinOrganization(action): IterableIterator { } } -export function* getDownloadList(): IterableIterator { +export function* getDownloadList() { try { const result = yield call(request, `${api.download}/page`) yield put(downloadListLoaded(result.payload)) @@ -377,7 +377,7 @@ export function* getDownloadList(): IterableIterator { } } -export function* downloadFile(action): IterableIterator { +export function* downloadFile(action) { const { id } = action.payload try { location.href = `${api.download}/record/file/${id}/${getToken()}` @@ -388,7 +388,7 @@ export function* downloadFile(action): IterableIterator { } } -export function* getUserByToken(action): IterableIterator { +export function* getUserByToken(action) { const { token } = action.payload try { const result = yield call(request, `${api.user}/check/${token}`) @@ -401,7 +401,7 @@ export function* getUserByToken(action): IterableIterator { } } -export default function* rootGroupSaga(): IterableIterator { +export default function* rootGroupSaga() { yield all([ throttle(1000, CHECK_NAME, checkNameUnique as any), takeEvery(ACTIVE, activeUser as any), diff --git a/webapp/app/containers/Dashboard/sagas.ts b/webapp/app/containers/Dashboard/sagas.ts index 880a6d18a..be18bf006 100644 --- a/webapp/app/containers/Dashboard/sagas.ts +++ b/webapp/app/containers/Dashboard/sagas.ts @@ -618,7 +618,7 @@ export function* getWidgetCsv(action: DashboardActionType) { } } -export default function* rootDashboardSaga(): IterableIterator { +export default function* rootDashboardSaga() { yield all([ takeLatest(ActionTypes.LOAD_DASHBOARD_DETAIL, getDashboardDetail), takeEvery(ActionTypes.ADD_DASHBOARD_ITEMS, addDashboardItems), diff --git a/webapp/app/containers/Profile/sagas.ts b/webapp/app/containers/Profile/sagas.ts index 4bfb57b63..1e167e89f 100644 --- a/webapp/app/containers/Profile/sagas.ts +++ b/webapp/app/containers/Profile/sagas.ts @@ -30,7 +30,7 @@ import request from 'utils/request' import api from 'utils/api' import { errorHandler } from 'utils/util' -export function* getUserProfile (action): IterableIterator { +export function* getUserProfile (action) { const { id } = action.payload try { @@ -47,7 +47,7 @@ export function* getUserProfile (action): IterableIterator { } -export default function* rootGroupSaga (): IterableIterator { +export default function* rootGroupSaga () { yield all([ takeLatest(GET_USER_PROFILE, getUserProfile as any) ]) diff --git a/webapp/app/containers/Projects/sagas.ts b/webapp/app/containers/Projects/sagas.ts index e6e5fa4a7..fa3ae8383 100644 --- a/webapp/app/containers/Projects/sagas.ts +++ b/webapp/app/containers/Projects/sagas.ts @@ -396,7 +396,7 @@ export function* excludeRole (action: ProjectActionType) { } } -export default function* rootProjectSaga (): IterableIterator { +export default function* rootProjectSaga () { yield all([ takeLatest(ActionTypes.LOAD_PROJECTS, getProjects), takeLatest(ActionTypes.ADD_PROJECT_ROLE, addProjectRole), diff --git a/webapp/app/containers/Register/sagas.ts b/webapp/app/containers/Register/sagas.ts index f86e1c3e5..b4f23b50b 100644 --- a/webapp/app/containers/Register/sagas.ts +++ b/webapp/app/containers/Register/sagas.ts @@ -6,7 +6,7 @@ import { errorHandler } from 'utils/util' import { call, put, all, takeLatest } from 'redux-saga/effects' -export function* signup (action): IterableIterator { +export function* signup (action) { const {username, email, password, resolve} = action.payload try { const asyncData = yield call(request, { @@ -26,7 +26,7 @@ export function* signup (action): IterableIterator { errorHandler(err) } } -export function* sendMailAgain (action): IterableIterator { +export function* sendMailAgain (action) { const {email, resolve} = action.payload try { const asyncData = yield call(request, { @@ -47,7 +47,7 @@ export function* sendMailAgain (action): IterableIterator { -export default function* rootGroupSaga (): IterableIterator { +export default function* rootGroupSaga () { yield all([ takeLatest(SIGNUP, signup as any), takeLatest(SEND_MAIL_AGAIN, sendMailAgain as any) diff --git a/webapp/app/containers/Viz/sagas.ts b/webapp/app/containers/Viz/sagas.ts index ae81132ba..b7230ba8f 100644 --- a/webapp/app/containers/Viz/sagas.ts +++ b/webapp/app/containers/Viz/sagas.ts @@ -546,7 +546,7 @@ export function* deleteSlides(action: VizActionType) { } } -export default function* rootVizSaga(): IterableIterator { +export default function* rootVizSaga() { yield all([ takeLatest(ActionTypes.LOAD_PORTALS, getPortals), takeEvery(ActionTypes.ADD_PORTAL, addPortal), diff --git a/webapp/share/containers/Dashboard/sagas.ts b/webapp/share/containers/Dashboard/sagas.ts index c373fadf0..e975b0234 100644 --- a/webapp/share/containers/Dashboard/sagas.ts +++ b/webapp/share/containers/Dashboard/sagas.ts @@ -469,7 +469,7 @@ export function* initiateDownloadTask(action: DashboardActionType) { } } -export default function* rootDashboardSaga(): IterableIterator { +export default function* rootDashboardSaga() { yield all([ takeLatest(ActionTypes.LOAD_SHARE_DASHBOARD, getDashboard), takeEvery(ActionTypes.LOAD_SHARE_WIDGET, getWidget), diff --git a/webapp/share/containers/Display/sagas.ts b/webapp/share/containers/Display/sagas.ts index cbe5eb098..aeacd4e41 100644 --- a/webapp/share/containers/Display/sagas.ts +++ b/webapp/share/containers/Display/sagas.ts @@ -132,7 +132,7 @@ export function* getData (action: ShareDisplayActionType) { } } -export default function* rootDisplaySaga (): IterableIterator { +export default function* rootDisplaySaga () { yield all([ takeLatest(ActionTypes.LOAD_SHARE_DISPLAY, getDisplay), takeEvery(ActionTypes.LOAD_LAYER_DATA, getData) From 773f165af6e7cf477dbc64673d54fbfe142aac24 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Tue, 27 Oct 2020 17:12:22 +0800 Subject: [PATCH 13/18] fix: test case errors --- .../app/containers/Display/actions.test.ts | 16 ++--- .../test/app/containers/Display/fixtures.ts | 60 ++++++++++++------- .../app/containers/Display/reducer.test.ts | 14 ++--- .../test/app/containers/Display/sagas.test.ts | 6 +- 4 files changed, 58 insertions(+), 38 deletions(-) diff --git a/webapp/test/app/containers/Display/actions.test.ts b/webapp/test/app/containers/Display/actions.test.ts index 1151b7dca..b2039ed78 100644 --- a/webapp/test/app/containers/Display/actions.test.ts +++ b/webapp/test/app/containers/Display/actions.test.ts @@ -25,7 +25,7 @@ import { mockSlideId, mockGraphLayerFormed, mockWidgetFormed, - mockView, + mockFormedViews, mockHttpError, mockCover, mockSlide, @@ -48,8 +48,8 @@ import { mockShareLinkParams, mockShareToken, mockAuthShareToken, - mockPwdToken, - mockPwd, + mockPasswordToken, + mockPassword, mockDisplayTitle } from './fixtures' @@ -77,7 +77,7 @@ describe('Display Actions', () => { slideId: mockSlideId, layers: [mockGraphLayerFormed], widgets: [mockWidgetFormed], - views: [mockView] + formedViews: mockFormedViews } } expect( @@ -85,7 +85,7 @@ describe('Display Actions', () => { mockSlideId, [mockGraphLayerFormed], [mockWidgetFormed], - [mockView] + mockFormedViews ) ).toEqual(expectedResult) }) @@ -556,12 +556,12 @@ describe('Display Actions', () => { const expectedResult = { type: ActionTypes.LOAD_DISPLAY_PASSWORD_SHARE_LINK_SUCCESS, payload: { - pwdToken: mockPwdToken, - pwd: mockPwd + passwordShareToken: mockPasswordToken, + password: mockPassword } } expect( - actions.displayPasswordShareLinkLoaded(mockPwdToken, mockPwd) + actions.displayPasswordShareLinkLoaded(mockPasswordToken, mockPassword) ).toEqual(expectedResult) }) }) diff --git a/webapp/test/app/containers/Display/fixtures.ts b/webapp/test/app/containers/Display/fixtures.ts index 7ed6c3b28..e7a76aa04 100644 --- a/webapp/test/app/containers/Display/fixtures.ts +++ b/webapp/test/app/containers/Display/fixtures.ts @@ -23,7 +23,7 @@ import { ILayerParams } from 'app/containers/Display/components/types' -import { IView } from 'app/containers/View/types' +import { IFormedViews, IView } from 'app/containers/View/types' import { IDisplayState, IDisplaySharePanelState, @@ -37,6 +37,11 @@ import { displayInitialState } from 'app/containers/Display/reducer' import { appInitialState } from 'app/containers/App/reducer' import { viewInitialState } from 'app/containers/View/reducer' import { IVizState } from 'app/containers/Viz/types' +import { IShareTokenParams } from 'app/components/SharePanel/types' +import { + ViewModelTypes, + ViewModelVisualTypes +} from 'app/containers/View/constants' export const mockDisplayId: number = 72 export const mockSlideId: number = 627 @@ -75,7 +80,7 @@ export const Req = export const mockAuthShareToken = 'eNoVjskRBDEIxFIazoYnBpN_SOv9Syp5RcR-Yo11zerL6o4oxNbFFKxCSAyUPTr57W5Ysto0nSSXM0pfqt4RhWJSM4UT7PjzT8uKV9DnJt_tSEJKfK--eZs0LilMittsZe5UvL7nW3C7x-EF7uMn4nlRxnyMqPV8mmu4B2BzNmQ8alU6dBy0_gM8rDKA' -export const mockPwdToken = +export const mockPasswordToken = 'eNoVzrkBAzEIBMCWED8hQmz_JZ0dTD7emQkSm4Br9Syre2RHojcepwomni3LC6adhRz48ZGT8ISk0S3fjEzCUB9G14z5ueEGU9C03eSWcCe-oPjJwpOunaOg' export const mockHttpError = new Error('Request failed with status code 403') @@ -95,12 +100,12 @@ export const mockDeltaSize = { deltaHeight: 0 } -export const mockCover = new Blob([''], { +export const mockCover = new Blob([''], { type: 'image/png' }) export const mockShareToken = 'eNoNybkBwDAIBLCVAGM4Ssyz_0hJpUKWAJbOLV_TyBpRM0c6Nscbr9jHmlmzrNEjTLtZOOYVI61sTwr0opc0wZO0NEJT_q8QXVylZvP4jbMmRYuRKsgHZMwdFA' -export const mockPwd = 'RYO92FBC' +export const mockPassword = 'RYO92FBC' export const mockShareTokenReq = { password: '', token: @@ -112,12 +117,13 @@ export const defaultSharePanelState: IDisplaySharePanelState = { title: '', visible: false } -export const mockShareLinkParams = { +export const mockShareLinkParams: IShareTokenParams = { id: 72, mode: 'AUTH', + expired: '2030-01-01', permission: 'SHARER', roles: null, - viewerIds: null + viewers: null } export const mockBaseLines: IBaseline = { @@ -129,18 +135,30 @@ export const mockBaseLines: IBaseline = { adjust: [0, 0] } -export const mockView: IView = { - roles: [], - name: '渠道信息', - projectId: 41, - sourceId: 53, - sql: 'SELECT * from dad', - variable: '[]', - config: '', - description: '', - id: 127, - model: - '{"name_level1":{"sqlType":"VARCHAR","visualType":"string","modelType":"category"},"总停留时间":{"sqlType":"DECIMAL","visualType":"number","modelType":"value"},"name_level2":{"sqlType":"VARCHAR","visualType":"string","modelType":"category"},"总调出次数":{"sqlType":"DECIMAL","visualType":"number","modelType":"value"},"name_level3":{"sqlType":"VARCHAR","visualType":"string","modelType":"category"},"platform":{"sqlType":"VARCHAR","visualType":"string","modelType":"category"},"总访问次数":{"sqlType":"DECIMAL","visualType":"number","modelType":"value"},"QD_id":{"sqlType":"VARCHAR","visualType":"string","modelType":"category"},"总页数":{"sqlType":"DECIMAL","visualType":"number","modelType":"value"}}' +export const mockFormedViews: IFormedViews = { + 127: { + id: 127, + roles: [], + name: '渠道信息', + projectId: 41, + sourceId: 53, + sql: 'SELECT * from dad', + variable: [], + config: '', + description: '', + model: { + name_level1: { + sqlType: 'VARCHAR', + visualType: ViewModelVisualTypes.String, + modelType: ViewModelTypes.Category + }, + 总停留时间: { + sqlType: 'DECIMAL', + visualType: ViewModelVisualTypes.Number, + modelType: ViewModelTypes.Value + } + } + } } export const mockSlideParams: ISlideParams = { @@ -389,13 +407,13 @@ export const mockViewItem = { config: '', description: '演示-人员信息', id: 84, - model: '', + model: '{}', name: '人员信息', projectId: 41, sourceId: 53, sql: 'SELECT * from personinfo where 1=1↵$if(name)$↵ and name = $name$↵$endif$↵$if(nation)$↵ and nation = $nation$↵$endif$↵$if(education)$↵ and education in ($education$)↵$endif$↵$if(city)$↵ and city in ($city$)↵$endif$', - variable: '', + variable: '[]', roles: [] } @@ -511,5 +529,3 @@ export const mockAppState = appInitialState export const mockViewState = { view: viewInitialState } - - diff --git a/webapp/test/app/containers/Display/reducer.test.ts b/webapp/test/app/containers/Display/reducer.test.ts index 24ee1adc8..694fc027b 100644 --- a/webapp/test/app/containers/Display/reducer.test.ts +++ b/webapp/test/app/containers/Display/reducer.test.ts @@ -31,8 +31,8 @@ import { mockGraphLayerFormed, mockSlideLayersOperationInfo, mockWidgetFormed, - mockPwdToken, - mockPwd, + mockPasswordToken, + mockPassword, mockChangedOperationInfo, mockDeltaSize, mockFinish, @@ -45,7 +45,7 @@ import { mockShareToken, mockAuthShareToken, mockDisplayTitle, - mockView, + mockFormedViews, mockCurrentDisplayWidgets, mockDefaultSlideLayersOperationGraphInfo, mockGraphLayerId, @@ -94,7 +94,7 @@ describe('displayReducer', () => { mockSlideId, [mockGraphLayerFormed], [mockWidgetFormed], - [mockView] + mockFormedViews ) ) ).toEqual(expectedResult) @@ -127,14 +127,14 @@ describe('displayReducer', () => { it('should handle the displayPasswordShareLinkLoaded action correctly', () => { const expectedResult = produce(state, (draft) => { - draft.currentDisplayPasswordShareToken = mockPwdToken - draft.currentDisplayPasswordPassword = mockPwd + draft.currentDisplayPasswordShareToken = mockPasswordToken + draft.currentDisplayPasswordPassword = mockPassword draft.loading.shareToken = false }) expect( reducer( state, - actions.displayPasswordShareLinkLoaded(mockPwdToken, mockPwd) + actions.displayPasswordShareLinkLoaded(mockPasswordToken, mockPassword) ) ).toEqual(expectedResult) }) diff --git a/webapp/test/app/containers/Display/sagas.test.ts b/webapp/test/app/containers/Display/sagas.test.ts index cf1b88528..ec26e4304 100644 --- a/webapp/test/app/containers/Display/sagas.test.ts +++ b/webapp/test/app/containers/Display/sagas.test.ts @@ -82,7 +82,11 @@ describe('getSlideDetail Saga', () => { mockSlideId, [mockGraphLayerFormed], [mockWidgetFormed], - [mockViewItem] + {[mockWidgetFormed.viewId]: { + ...mockViewItem, + model: {}, + variable: [] + }} ) ) .run() From 1d84921751b0fad9987a1212ec34a2323f36c489 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Tue, 27 Oct 2020 17:14:50 +0800 Subject: [PATCH 14/18] fix: TSLint rules compatible with prettier --- webapp/jest.config.js | 1 - webapp/tslint.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/jest.config.js b/webapp/jest.config.js index 4fdaeca28..a387fdcf3 100644 --- a/webapp/jest.config.js +++ b/webapp/jest.config.js @@ -5,7 +5,6 @@ module.exports = { coverageDirectory: './coverage', collectCoverageFrom: [ 'app/**/*.{ts,tsx}', - '!app/**/*.test.{ts,tsx}', '!app/app.tsx', '!app/*/*/Loadable.{ts,tsx}' ], diff --git a/webapp/tslint.json b/webapp/tslint.json index 82324a955..ea7758c26 100644 --- a/webapp/tslint.json +++ b/webapp/tslint.json @@ -5,7 +5,7 @@ "indent": [ true, "spaces" ], "quotemark": [ true, "single", "jsx-double" ], "trailing-comma": [ true, { "multiline": "never", "singleline": "never" } ], - "space-before-function-paren": [true, "always"], + "space-before-function-paren": [true, "never"], "no-submodule-imports": false, "only-arrow-functions": false, "prefer-for-of": false, From 95b3129879aa7a5e60b7b45fa7eb4eee94649a01 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Tue, 27 Oct 2020 19:46:59 +0800 Subject: [PATCH 15/18] refactor: use getServerConfiguration instead of getVersion --- webapp/app/containers/App/actions.ts | 49 ++++++++++-------- webapp/app/containers/App/constants.ts | 6 +-- webapp/app/containers/App/index.tsx | 24 ++++----- webapp/app/containers/App/reducer.ts | 23 ++++++--- webapp/app/containers/App/sagas.ts | 66 +++++++++++++----------- webapp/app/containers/App/types.ts | 12 ++++- webapp/app/containers/Login/index.tsx | 7 +-- webapp/app/globalConstants.ts | 5 +- webapp/app/utils/api.ts | 2 +- webapp/app/utils/request.ts | 9 +++- webapp/share/containers/App/actions.ts | 32 ++++++++++-- webapp/share/containers/App/constants.ts | 7 ++- webapp/share/containers/App/index.tsx | 1 + webapp/share/containers/App/sagas.ts | 28 +++++++++- 14 files changed, 178 insertions(+), 93 deletions(-) diff --git a/webapp/app/containers/App/actions.ts b/webapp/app/containers/App/actions.ts index dab8fd34d..b5630799d 100644 --- a/webapp/app/containers/App/actions.ts +++ b/webapp/app/containers/App/actions.ts @@ -23,9 +23,9 @@ import { GET_EXTERNAL_AUTH_PROVIDERS_SUCESS, TRY_EXTERNAL_AUTH, EXTERNAL_AUTH_LOGOUT, - GET_VERSION, - GET_VERSION_SUCCESS, - GET_VERSION_FAIL, + GET_SERVER_CONFIGURATIONS, + GET_SERVER_CONFIGURATIONS_SUCCESS, + GET_SERVER_CONFIGURATIONS_FAIL, LOGIN, LOGGED, LOGIN_ERROR, @@ -63,9 +63,13 @@ import { GET_USER_BY_TOKEN_FAIL } from './constants' -import { IGetgetCaptchaParams, IResetPasswordParams } from '../FindPassword/types' +import { + IGetgetCaptchaParams, + IResetPasswordParams +} from '../FindPassword/types' import { IReduxActionStruct } from 'utils/types' +import { IServerConfigurations } from './types' export function getExternalAuthProviders() { return { @@ -107,28 +111,29 @@ export function login(username, password, resolve) { } } -export function getVersion(resolve?) { +export function getServerConfigurations() { return { - type: GET_VERSION, - payload: { - resolve - } + type: GET_SERVER_CONFIGURATIONS } } -export function getVersionSuccess (version) { +export function serverConfigurationsGetted( + configurations: IServerConfigurations +) { return { - type: GET_VERSION_SUCCESS, + type: GET_SERVER_CONFIGURATIONS_SUCCESS, payload: { - version + configurations } } } -export function getVersionFail(err) { +export function getServerConfigurationsFail(error) { return { - type: GET_VERSION_FAIL, - payload: {err} + type: GET_SERVER_CONFIGURATIONS_FAIL, + payload: { + error + } } } @@ -356,7 +361,7 @@ export function downloadFileFail(error) { } } -export function getCaptchaforResetPassword ( +export function getCaptchaforResetPassword( params: IGetgetCaptchaParams ): IReduxActionStruct { return { @@ -383,7 +388,7 @@ export function getCaptchaforResetPasswordError(error) { } } -export function resetPasswordUnlogged ( +export function resetPasswordUnlogged( params: IResetPasswordParams ): IReduxActionStruct { return { @@ -392,7 +397,7 @@ export function resetPasswordUnlogged ( } } -export function resetPasswordUnloggedSuccess (result) { +export function resetPasswordUnloggedSuccess(result) { return { type: RESET_PASSWORD_UNLOGGED_SUCCESS, payload: { @@ -401,7 +406,7 @@ export function resetPasswordUnloggedSuccess (result) { } } -export function resetPasswordUnloggedFail (error) { +export function resetPasswordUnloggedFail(error) { return { type: RESET_PASSWORD_UNLOGGED_ERROR, payload: { @@ -410,7 +415,7 @@ export function resetPasswordUnloggedFail (error) { } } -export function getUserByToken (token) { +export function getUserByToken(token) { return { type: GET_USER_BY_TOKEN, payload: { @@ -419,7 +424,7 @@ export function getUserByToken (token) { } } -export function getUserByTokenSuccess (user) { +export function getUserByTokenSuccess(user) { return { type: GET_USER_BY_TOKEN_SUCCESS, payload: { @@ -428,7 +433,7 @@ export function getUserByTokenSuccess (user) { } } -export function getUserByTokenFail (error) { +export function getUserByTokenFail(error) { return { type: GET_USER_BY_TOKEN_FAIL, payload: { diff --git a/webapp/app/containers/App/constants.ts b/webapp/app/containers/App/constants.ts index 46d5debfd..c258ab787 100644 --- a/webapp/app/containers/App/constants.ts +++ b/webapp/app/containers/App/constants.ts @@ -83,9 +83,9 @@ export const UPDATE_TEAM_PROJECT_PERMISSION = 'davinci/permission/UPDATE_TEAM_PR export const UPDATE_TEAM = 'davinci/permission/UPDATE_TEAM' export const DELETE_TEAM = 'davinci/permission/DELETE_TEAM' -export const GET_VERSION = 'davinci/GET_VERSION' -export const GET_VERSION_SUCCESS = 'davinci/GET_VERSION_SUCCESS' -export const GET_VERSION_FAIL = 'davinci/GET_VERSION_FAIL' +export const GET_SERVER_CONFIGURATIONS = 'davinci/GET_SERVER_CONFIGURATIONS' +export const GET_SERVER_CONFIGURATIONS_SUCCESS = 'davinci/GET_SERVER_CONFIGURATIONS_SUCCESS' +export const GET_SERVER_CONFIGURATIONS_FAIL = 'davinci/GET_SERVER_CONFIGURATIONS_FAIL' export const GET_USER_BY_TOKEN = 'davinci/GET_USER_BY_TOKEN' export const GET_USER_BY_TOKEN_SUCCESS = 'davinci/GET_USER_BY_TOKEN_SUCCESS' diff --git a/webapp/app/containers/App/index.tsx b/webapp/app/containers/App/index.tsx index 818df7c8e..5260c3833 100644 --- a/webapp/app/containers/App/index.tsx +++ b/webapp/app/containers/App/index.tsx @@ -18,15 +18,15 @@ * >> */ -import React, { useCallback } from 'react' +import React from 'react' import Helmet from 'react-helmet' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' -import { Route, HashRouter as Router, Switch, Redirect, withRouter } from 'react-router-dom' +import { Route, HashRouter as Router, Switch, Redirect } from 'react-router-dom' import { RouteComponentWithParams } from 'utils/types' import { compose } from 'redux' -import { logged, logout, getUserByToken } from './actions' +import { logged, logout, getServerConfigurations, getUserByToken } from './actions' import injectReducer from 'utils/injectReducer' import reducer from './reducer' import injectSaga from 'utils/injectSaga' @@ -43,22 +43,15 @@ import { Background } from 'containers/Background/Loadable' import { Main } from 'containers/Main/Loadable' import { Activate } from 'containers/Register/Loadable' -interface IAppStateProps { - logged: boolean -} - -interface IAppDispatchProps { - onLogged: (user) => void - onLogout: () => void - onGetLoginUser: (token: string) => any -} - -type AppProps = IAppStateProps & IAppDispatchProps & RouteComponentWithParams +type MappedStates = ReturnType +type MappedDispatches = ReturnType +type AppProps = MappedStates & MappedDispatches & RouteComponentWithParams export class App extends React.PureComponent { constructor (props: AppProps) { super(props) + props.onGetServerConfigurations() this.checkTokenLink() } @@ -169,7 +162,8 @@ const mapStateToProps = createStructuredSelector({ const mapDispatchToProps = (dispatch) => ({ onLogged: (user) => dispatch(logged(user)), onLogout: () => dispatch(logout()), - onGetLoginUser: (token) => dispatch(getUserByToken(token)) + onGetLoginUser: (token: string) => dispatch(getUserByToken(token)), + onGetServerConfigurations: () => dispatch(getServerConfigurations()) }) const withConnect = connect( diff --git a/webapp/app/containers/App/reducer.ts b/webapp/app/containers/App/reducer.ts index 03c9c9ed3..63f7b8fcd 100644 --- a/webapp/app/containers/App/reducer.ts +++ b/webapp/app/containers/App/reducer.ts @@ -39,10 +39,9 @@ import { UPDATE_PROFILE_SUCCESS, GET_EXTERNAL_AUTH_PROVIDERS_SUCESS, DownloadStatus, - GET_VERSION_SUCCESS + GET_SERVER_CONFIGURATIONS_SUCCESS } from './constants' - const initialState = { externalAuthProviders: null, logged: null, @@ -52,7 +51,8 @@ const initialState = { downloadListLoading: false, downloadList: null, downloadListInfo: null, - version: null + version: '', + oauth2Enabled: false } const appReducer = (state = initialState, action) => @@ -76,8 +76,10 @@ const appReducer = (state = initialState, action) => draft.logged = true draft.loginUser = action.payload.user break - case GET_VERSION_SUCCESS: - draft.version = action.payload.version + case GET_SERVER_CONFIGURATIONS_SUCCESS: + draft.version = action.payload.configurations.version + draft.oauth2Enabled = + action.payload.configurations.security.oauth2.enable break case LOGOUT: draft.logged = false @@ -88,7 +90,13 @@ const appReducer = (state = initialState, action) => break case UPDATE_PROFILE_SUCCESS: const { id, name, department, description } = action.payload.user - draft.loginUser = { ...draft.loginUser, id, name, department, description } + draft.loginUser = { + ...draft.loginUser, + id, + name, + department, + description + } break case SHOW_NAVIGATOR: draft.navigator = true @@ -113,7 +121,8 @@ const appReducer = (state = initialState, action) => draft.downloadListLoading = false break case DOWNLOAD_FILE_SUCCESS: - draft.downloadList.find(({ id }) => action.payload.id === id).status = DownloadStatus.Downloaded + draft.downloadList.find(({ id }) => action.payload.id === id).status = + DownloadStatus.Downloaded break } }) diff --git a/webapp/app/containers/App/sagas.ts b/webapp/app/containers/App/sagas.ts index bebef5cb6..f1da7f763 100644 --- a/webapp/app/containers/App/sagas.ts +++ b/webapp/app/containers/App/sagas.ts @@ -32,7 +32,7 @@ import { LOGOUT, CHECK_NAME, ACTIVE, - GET_VERSION, + GET_SERVER_CONFIGURATIONS, UPDATE_PROFILE, CHANGE_USER_PASSWORD, JOIN_ORGANIZATION, @@ -46,8 +46,6 @@ import { GET_USER_BY_TOKEN } from './constants' import { - logged, - getVersion, loginError, activeSuccess, activeError, @@ -66,17 +64,26 @@ import { getCaptchaforResetPasswordError, resetPasswordUnloggedSuccess, resetPasswordUnloggedFail, - getVersionSuccess, - getVersionFail, + serverConfigurationsGetted, + getServerConfigurationsFail, getUserByTokenFail, getUserByTokenSuccess } from './actions' -import request, { removeToken, getToken } from 'utils/request' +import request, { + removeToken, + getToken, + setTokenExpired, + IDavinciResponse +} from 'utils/request' import { errorHandler } from 'utils/util' import api from 'utils/api' import { IReduxActionStruct } from 'utils/types' -import { IResetPasswordParams, IGetgetCaptchaParams } from '../FindPassword/types' +import { + IResetPasswordParams, + IGetgetCaptchaParams +} from '../FindPassword/types' +import { IServerConfigurations } from './types' export function* getExternalAuthProviders() { try { @@ -92,19 +99,20 @@ export function* getExternalAuthProviders() { } } -export function* getDavinciVersion(action) { - const {resolve} = action.payload +export function* getServerConfigurations(action) { try { - const version = yield call(request, { - method: 'get', - url: api.version - }) - yield put(getVersionSuccess(version)) - if (resolve) { - resolve(version) - } + const result: IDavinciResponse = yield call( + request, + { + method: 'get', + url: api.configurations + } + ) + const configurations = result.payload + setTokenExpired(configurations.jwtToken.timeout) + yield put(serverConfigurationsGetted(configurations)) } catch (err) { - yield put(getVersionFail(err)) + yield put(getServerConfigurationsFail(err)) errorHandler(err) } } @@ -403,24 +411,24 @@ export function* getUserByToken(action) { export default function* rootGroupSaga() { yield all([ - throttle(1000, CHECK_NAME, checkNameUnique as any), - takeEvery(ACTIVE, activeUser as any), - takeLatest(GET_EXTERNAL_AUTH_PROVIDERS, getExternalAuthProviders as any), - takeEvery(TRY_EXTERNAL_AUTH, tryExternalAuth as any), - takeEvery(EXTERNAL_AUTH_LOGOUT, externalAuthlogout as any), - takeEvery(LOGIN, login as any), + throttle(1000, CHECK_NAME, checkNameUnique), + takeEvery(ACTIVE, activeUser), + takeLatest(GET_EXTERNAL_AUTH_PROVIDERS, getExternalAuthProviders), + takeEvery(TRY_EXTERNAL_AUTH, tryExternalAuth), + takeEvery(EXTERNAL_AUTH_LOGOUT, externalAuthlogout), + takeEvery(LOGIN, login), takeEvery(LOGOUT, logout), - takeEvery(UPDATE_PROFILE, updateProfile as any), + takeEvery(UPDATE_PROFILE, updateProfile), takeEvery(CHANGE_USER_PASSWORD, changeUserPassword as any), takeEvery( GET_CAPTCHA_FOR_RESET_PASSWORD, getCaptchaForResetPassword as any ), - takeEvery(RESET_PASSWORD_UNLOGGED, resetPasswordUnlogged as any), - takeEvery(GET_USER_BY_TOKEN, getUserByToken as any), - takeEvery(JOIN_ORGANIZATION, joinOrganization as any), + takeEvery(RESET_PASSWORD_UNLOGGED, resetPasswordUnlogged as any), + takeEvery(GET_USER_BY_TOKEN, getUserByToken), + takeEvery(JOIN_ORGANIZATION, joinOrganization), takeLatest(LOAD_DOWNLOAD_LIST, getDownloadList), takeLatest(DOWNLOAD_FILE, downloadFile), - takeLatest(GET_VERSION, getDavinciVersion) + takeLatest(GET_SERVER_CONFIGURATIONS, getServerConfigurations) ]) } diff --git a/webapp/app/containers/App/types.ts b/webapp/app/containers/App/types.ts index 2ff8fbf44..df5886b19 100644 --- a/webapp/app/containers/App/types.ts +++ b/webapp/app/containers/App/types.ts @@ -8,4 +8,14 @@ export interface IDownloadRecord { uuid?: string } - +export interface IServerConfigurations { + version: string + jwtToken: { + timeout: number + } + security: { + oauth2: { + enable: boolean + } + } +} diff --git a/webapp/app/containers/Login/index.tsx b/webapp/app/containers/Login/index.tsx index 0ad963fb3..944d85376 100644 --- a/webapp/app/containers/Login/index.tsx +++ b/webapp/app/containers/Login/index.tsx @@ -28,7 +28,7 @@ import LoginForm from './LoginForm' import { compose } from 'redux' -import { login, logged, getVersion } from '../App/actions' +import { login, logged } from '../App/actions' import { makeSelectLoginLoading } from '../App/selectors' import checkLogin from 'utils/checkLogin' import { setToken } from 'utils/request' @@ -40,7 +40,6 @@ const styles = require('./Login.less') interface ILoginProps { loginLoading: boolean onLogged: (user) => void - onGetVersion: (resolve?: (version: string) => void) => void onLogin: (username: string, password: string, resolve: () => any) => any } @@ -62,7 +61,6 @@ export class Login extends React.PureComponent< } public componentWillMount() { - this.props.onGetVersion() this.checkNormalLogin() } @@ -167,8 +165,7 @@ export function mapDispatchToProps(dispatch) { return { onLogin: (username, password, resolve) => dispatch(login(username, password, resolve)), - onLogged: (user) => dispatch(logged(user)), - onGetVersion: (resolve) => dispatch(getVersion(resolve)) + onLogged: (user) => dispatch(logged(user)) } } diff --git a/webapp/app/globalConstants.ts b/webapp/app/globalConstants.ts index 1b8b0e67f..bae3b09a5 100644 --- a/webapp/app/globalConstants.ts +++ b/webapp/app/globalConstants.ts @@ -206,5 +206,6 @@ export const DEFAULT_FONT_FAMILY = '"Chinese Quote", -apple-system, BlinkMacSyst export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD' export const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' -export const DOWNLOAD_LIST_POLLING_FREQUENCY = 30000 -export const DEFAULT_CACHE_EXPIRED = 300 +export const DEFAULT_JWT_TOKEN_EXPIRED = 60 * 60 * 1000 // ms +export const DOWNLOAD_LIST_POLLING_FREQUENCY = 30000 // ms +export const DEFAULT_CACHE_EXPIRED = 300 // sec diff --git a/webapp/app/utils/api.ts b/webapp/app/utils/api.ts index bb9341707..43a24cbd2 100644 --- a/webapp/app/utils/api.ts +++ b/webapp/app/utils/api.ts @@ -47,5 +47,5 @@ export default { star: `${API_HOST}/star`, download: `${API_HOST}/download`, buriedPoints: `${API_HOST}/statistic`, - version: `${API_HOST}/version` + configurations: `${API_HOST}/configurations` } diff --git a/webapp/app/utils/request.ts b/webapp/app/utils/request.ts index 270560a1b..8208664ec 100644 --- a/webapp/app/utils/request.ts +++ b/webapp/app/utils/request.ts @@ -19,6 +19,9 @@ */ import axios, { AxiosRequestConfig, AxiosResponse, AxiosPromise } from 'axios' +import { DEFAULT_JWT_TOKEN_EXPIRED } from 'app/globalConstants' + +let tokenExpired = DEFAULT_JWT_TOKEN_EXPIRED axios.defaults.validateStatus = function (status) { return status < 400 @@ -48,7 +51,7 @@ export default function request (url: string | AxiosRequestConfig, options?: Axi export function setToken (token: string) { localStorage.setItem('TOKEN', token) - localStorage.setItem('TOKEN_EXPIRE', `${new Date().getTime() + 3600000}`) + localStorage.setItem('TOKEN_EXPIRE', `${new Date().getTime() + tokenExpired}`) axios.defaults.headers.common['Authorization'] = `Bearer ${token}` } @@ -73,6 +76,10 @@ export function getToken () { return axios.defaults.headers.common['Authorization'] } +export function setTokenExpired(expired) { + tokenExpired = Number(expired) +} + window.addEventListener('storage', syncToken) interface IDavinciResponseHeader { diff --git a/webapp/share/containers/App/actions.ts b/webapp/share/containers/App/actions.ts index 4cb0407ba..6361a383a 100644 --- a/webapp/share/containers/App/actions.ts +++ b/webapp/share/containers/App/actions.ts @@ -20,6 +20,7 @@ import { ActionTypes } from './constants' import { returnType } from 'utils/redux' +import { IServerConfigurations } from 'app/containers/App/types' export const AppActions = { login(username, password, shareToken, resolve, reject?) { @@ -59,7 +60,7 @@ export const AppActions = { } }, - interceptor (token: string) { + interceptor(token: string) { return { type: ActionTypes.INTERCEPTOR_PREFLIGHT, payload: { @@ -84,7 +85,12 @@ export const AppActions = { } }, - getPermissions(token: string, password?: string, resolve?: () => void, reject?: () => void) { + getPermissions( + token: string, + password?: string, + resolve?: () => void, + reject?: () => void + ) { return { type: ActionTypes.GET_PERMISSIONS, payload: { @@ -107,8 +113,28 @@ export const AppActions = { return { type: ActionTypes.GET_PERMISSIONS_FAIL } + }, + getServerConfigurations() { + return { + type: ActionTypes.GET_SERVER_CONFIGURATIONS + } + }, + serverConfigurationsGetted(configurations: IServerConfigurations) { + return { + type: ActionTypes.GET_SERVER_CONFIGURATIONS_SUCCESS, + payload: { + configurations + } + } + }, + getServerConfigurationsFail(error) { + return { + type: ActionTypes.GET_SERVER_CONFIGURATIONS_FAIL, + payload: { + error + } + } } - } const mockAction = returnType(AppActions) diff --git a/webapp/share/containers/App/constants.ts b/webapp/share/containers/App/constants.ts index 891b4ad0c..518e6a7af 100644 --- a/webapp/share/containers/App/constants.ts +++ b/webapp/share/containers/App/constants.ts @@ -33,8 +33,11 @@ enum Types { GET_PERMISSIONS = 'davinci/Share/App/GET_PERMISSIONS', GET_PERMISSIONS_SUCCESS = 'davinci/Share/App/GET_PERMISSIONS_SUCCESS', GET_PERMISSIONS_FAIL = 'davinci/Share/App/GET_PERMISSIONS_FAIL', - PASSWORD_SHARE_TOKENS = 'PASSWORD_SHARE_TOKENS', - AUTH_SHARE_TOKEN = 'AUTH_SHARE_TOKEN' + PASSWORD_SHARE_TOKENS = 'davinci/Share/PASSWORD_SHARE_TOKENS', + AUTH_SHARE_TOKEN = 'davinci/Share/AUTH_SHARE_TOKEN', + GET_SERVER_CONFIGURATIONS = 'davinci/Share/GET_SERVER_CONFIGURATIONS', + GET_SERVER_CONFIGURATIONS_SUCCESS = 'davinci/Share/GET_SERVER_CONFIGURATIONS_SUCCESS', + GET_SERVER_CONFIGURATIONS_FAIL = 'davinci/Share/GET_SERVER_CONFIGURATIONS_FAIL' } export const ActionTypes = createTypes(Types) diff --git a/webapp/share/containers/App/index.tsx b/webapp/share/containers/App/index.tsx index 6748fc211..f7c8430cc 100644 --- a/webapp/share/containers/App/index.tsx +++ b/webapp/share/containers/App/index.tsx @@ -58,6 +58,7 @@ export const App: React.FC = () => { useEffect(() => { dispatch(AppActions.interceptor(shareToken)) + dispatch(AppActions.getServerConfigurations()) }, []) useEffect(() => { diff --git a/webapp/share/containers/App/sagas.ts b/webapp/share/containers/App/sagas.ts index e3f67ef51..c109692d0 100644 --- a/webapp/share/containers/App/sagas.ts +++ b/webapp/share/containers/App/sagas.ts @@ -23,8 +23,9 @@ import { call, put, all, takeLatest, takeEvery } from 'redux-saga/effects' import { ActionTypes } from './constants' import { AppActions, AppActionType } from './actions' -import request from 'utils/request' +import request, { IDavinciResponse, setTokenExpired } from 'utils/request' import { errorHandler } from 'utils/util' +import { IServerConfigurations } from 'app/containers/App/types' import api from 'utils/api' export function* login(action: AppActionType) { @@ -101,10 +102,33 @@ export function* getPermissions(action: AppActionType) { } } +export function* getServerConfigurations(action: AppActionType) { + if (action.type !== ActionTypes.GET_SERVER_CONFIGURATIONS) { + return + } + const { serverConfigurationsGetted, getServerConfigurationsFail } = AppActions + try { + const result: IDavinciResponse = yield call( + request, + { + method: 'get', + url: api.configurations + } + ) + const configurations = result.payload + setTokenExpired(configurations.jwtToken.timeout) + yield put(serverConfigurationsGetted(configurations)) + } catch (err) { + yield put(getServerConfigurationsFail(err)) + errorHandler(err) + } +} + export default function* rootAppSaga() { yield all([ takeLatest(ActionTypes.LOGIN, login), takeEvery(ActionTypes.INTERCEPTOR_PREFLIGHT, interceptor), - takeEvery(ActionTypes.GET_PERMISSIONS, getPermissions) + takeEvery(ActionTypes.GET_PERMISSIONS, getPermissions), + takeLatest(ActionTypes.GET_SERVER_CONFIGURATIONS, getServerConfigurations) ]) } From e9bc88982bbcd5d3a865f784f1919b0b864adc1a Mon Sep 17 00:00:00 2001 From: ScottSut Date: Wed, 28 Oct 2020 09:39:42 +0800 Subject: [PATCH 16/18] refactor(oauth2): remove external log out action --- webapp/app/containers/App/actions.ts | 6 ---- webapp/app/containers/App/constants.ts | 1 - webapp/app/containers/App/sagas.ts | 6 ---- webapp/app/containers/App/selectors.ts | 7 +++++ webapp/app/containers/Login/index.tsx | 22 ++++++++------- webapp/app/containers/Main/index.tsx | 38 ++++++++++---------------- webapp/app/globalConstants.ts | 1 + webapp/app/utils/api.ts | 1 - 8 files changed, 35 insertions(+), 47 deletions(-) diff --git a/webapp/app/containers/App/actions.ts b/webapp/app/containers/App/actions.ts index b5630799d..27047add4 100644 --- a/webapp/app/containers/App/actions.ts +++ b/webapp/app/containers/App/actions.ts @@ -22,7 +22,6 @@ import { GET_EXTERNAL_AUTH_PROVIDERS, GET_EXTERNAL_AUTH_PROVIDERS_SUCESS, TRY_EXTERNAL_AUTH, - EXTERNAL_AUTH_LOGOUT, GET_SERVER_CONFIGURATIONS, GET_SERVER_CONFIGURATIONS_SUCCESS, GET_SERVER_CONFIGURATIONS_FAIL, @@ -94,11 +93,6 @@ export function tryExternalAuth(resolve) { } } } -export function externalAuthlogout() { - return { - type: EXTERNAL_AUTH_LOGOUT - } -} export function login(username, password, resolve) { return { diff --git a/webapp/app/containers/App/constants.ts b/webapp/app/containers/App/constants.ts index c258ab787..867470157 100644 --- a/webapp/app/containers/App/constants.ts +++ b/webapp/app/containers/App/constants.ts @@ -21,7 +21,6 @@ export const GET_EXTERNAL_AUTH_PROVIDERS = 'davinci/App/GET_EXTERNAL_AUTH_PROVIDERS' export const GET_EXTERNAL_AUTH_PROVIDERS_SUCESS = 'davinci/App/GET_EXTERNAL_AUTH_PROVIDERS_SUCESS' export const TRY_EXTERNAL_AUTH = 'davinci/App/TRY_EXTERNAL_AUTH' -export const EXTERNAL_AUTH_LOGOUT = 'davinci/App/EXTERNAL_AUTH_LOGOUT' export const LOGIN = 'davinci/App/LOGIN' export const LOGGED = 'davinci/App/LOGGED' export const LOGIN_ERROR = 'davinci/App/LOGIN_ERROR' diff --git a/webapp/app/containers/App/sagas.ts b/webapp/app/containers/App/sagas.ts index f1da7f763..42ab6bce4 100644 --- a/webapp/app/containers/App/sagas.ts +++ b/webapp/app/containers/App/sagas.ts @@ -40,7 +40,6 @@ import { DOWNLOAD_FILE, GET_EXTERNAL_AUTH_PROVIDERS, TRY_EXTERNAL_AUTH, - EXTERNAL_AUTH_LOGOUT, GET_CAPTCHA_FOR_RESET_PASSWORD, RESET_PASSWORD_UNLOGGED, GET_USER_BY_TOKEN @@ -154,10 +153,6 @@ export function* login(action) { } } -export function* externalAuthlogout() { - location.replace(`${api.externalLogout}`) -} - export function* logout() { try { removeToken() @@ -415,7 +410,6 @@ export default function* rootGroupSaga() { takeEvery(ACTIVE, activeUser), takeLatest(GET_EXTERNAL_AUTH_PROVIDERS, getExternalAuthProviders), takeEvery(TRY_EXTERNAL_AUTH, tryExternalAuth), - takeEvery(EXTERNAL_AUTH_LOGOUT, externalAuthlogout), takeEvery(LOGIN, login), takeEvery(LOGOUT, logout), takeEvery(UPDATE_PROFILE, updateProfile), diff --git a/webapp/app/containers/App/selectors.ts b/webapp/app/containers/App/selectors.ts index 82825a9b5..bbfb2ce01 100644 --- a/webapp/app/containers/App/selectors.ts +++ b/webapp/app/containers/App/selectors.ts @@ -79,9 +79,16 @@ const makeSelectVersion = () => (globalState) => globalState.version ) +const makeSelectOauth2Enabled = () => + createSelector( + selectGlobal, + (globalState) => globalState.oauth2Enabled + ) + export { selectGlobal, makeSelectVersion, + makeSelectOauth2Enabled, makeSelectExternalAuthProviders, makeSelectLogged, makeSelectLoginUser, diff --git a/webapp/app/containers/Login/index.tsx b/webapp/app/containers/Login/index.tsx index 944d85376..e5a6a76fc 100644 --- a/webapp/app/containers/Login/index.tsx +++ b/webapp/app/containers/Login/index.tsx @@ -29,7 +29,10 @@ import LoginForm from './LoginForm' import { compose } from 'redux' import { login, logged } from '../App/actions' -import { makeSelectLoginLoading } from '../App/selectors' +import { + makeSelectLoginLoading, + makeSelectOauth2Enabled +} from '../App/selectors' import checkLogin from 'utils/checkLogin' import { setToken } from 'utils/request' import { statistic } from 'utils/statistic/statistic.dv' @@ -37,11 +40,9 @@ import ExternalLogin from '../ExternalLogin' const styles = require('./Login.less') -interface ILoginProps { - loginLoading: boolean - onLogged: (user) => void - onLogin: (username: string, password: string, resolve: () => any) => any -} +type MappedStates = ReturnType +type MappedDispatches = ReturnType +type ILoginProps = MappedStates & MappedDispatches interface ILoginStates { username: string @@ -122,7 +123,7 @@ export class Login extends React.PureComponent< } public render() { - const { loginLoading } = this.props + const { loginLoading, oauth2Enabled } = this.props const { username, password } = this.state return (
@@ -151,19 +152,20 @@ export class Login extends React.PureComponent< 忘记密码?

- + {oauth2Enabled && }
) } } const mapStateToProps = createStructuredSelector({ - loginLoading: makeSelectLoginLoading() + loginLoading: makeSelectLoginLoading(), + oauth2Enabled: makeSelectOauth2Enabled() }) export function mapDispatchToProps(dispatch) { return { - onLogin: (username, password, resolve) => + onLogin: (username: string, password: string, resolve: () => void) => dispatch(login(username, password, resolve)), onLogged: (user) => dispatch(logged(user)) } diff --git a/webapp/app/containers/Main/index.tsx b/webapp/app/containers/Main/index.tsx index fa96708e3..595ef559a 100644 --- a/webapp/app/containers/Main/index.tsx +++ b/webapp/app/containers/Main/index.tsx @@ -27,9 +27,9 @@ import { createStructuredSelector } from 'reselect' import Navigator from 'components/Navigator' -import { logged, logout, loadDownloadList, externalAuthlogout } from '../App/actions' -import { makeSelectLogged, makeSelectNavigator } from '../App/selectors' -import { DOWNLOAD_LIST_POLLING_FREQUENCY } from 'app/globalConstants' +import { logged, logout, loadDownloadList } from '../App/actions' +import { makeSelectLogged, makeSelectNavigator, makeSelectOauth2Enabled } from '../App/selectors' +import { DOWNLOAD_LIST_POLLING_FREQUENCY, EXTERNAL_LOG_OUT_URL } from 'app/globalConstants' import { Project, ProjectList } from 'containers/Projects/Loadable' @@ -53,19 +53,11 @@ import { NoAuthorization } from 'containers/NoAuthorization/Loadable' const styles = require('./Main.less') -interface IMainProps { - logged: boolean - navigator: boolean - onLogged: (user) => void - onLogout: () => void - onExternalAuthLogout: () => void - onLoadDownloadList: () => void -} +type MappedStates = ReturnType +type MappedDispatches = ReturnType +type IMainProps = MappedStates & MappedDispatches & RouteComponentWithParams -export class Main extends React.Component< - IMainProps & RouteComponentWithParams, - {} -> { +export class Main extends React.Component { private downloadListPollingTimer: number constructor(props: IMainProps & RouteComponentWithParams) { @@ -87,10 +79,13 @@ export class Main extends React.Component< } private logout = () => { - const { history, onLogout , onExternalAuthLogout } = this.props + const { history, oauth2Enabled, onLogout } = this.props onLogout() - onExternalAuthLogout() - history.replace('/login') + if (oauth2Enabled) { + history.replace(EXTERNAL_LOG_OUT_URL) + } else { + history.replace('/login') + } } private renderAccount = () => ( @@ -193,6 +188,7 @@ export class Main extends React.Component< const mapStateToProps = createStructuredSelector({ logged: makeSelectLogged(), + oauth2Enabled: makeSelectOauth2Enabled(), navigator: makeSelectNavigator() }) @@ -200,12 +196,8 @@ export function mapDispatchToProps(dispatch) { return { onLogged: (user) => dispatch(logged(user)), onLogout: () => dispatch(logout()), - onExternalAuthLogout: () => dispatch(externalAuthlogout()), onLoadDownloadList: () => dispatch(loadDownloadList()) } } -export default connect( - mapStateToProps, - mapDispatchToProps -)(Main) +export default connect(mapStateToProps, mapDispatchToProps)(Main) diff --git a/webapp/app/globalConstants.ts b/webapp/app/globalConstants.ts index bae3b09a5..f7fca8182 100644 --- a/webapp/app/globalConstants.ts +++ b/webapp/app/globalConstants.ts @@ -21,6 +21,7 @@ export const CLIENT_VERSION = '0.3-beta.9' export const API_HOST = '/api/v3' export const SHARE_HOST = `${location.origin}/share.html` +export const EXTERNAL_LOG_OUT_URL = '/login/oauth2/logout' const defaultEchartsTheme = require('assets/json/echartsThemes/default.project.json') export const DEFAULT_ECHARTS_THEME = defaultEchartsTheme.theme diff --git a/webapp/app/utils/api.ts b/webapp/app/utils/api.ts index 43a24cbd2..1ccd9168a 100644 --- a/webapp/app/utils/api.ts +++ b/webapp/app/utils/api.ts @@ -23,7 +23,6 @@ import { API_HOST } from '../globalConstants' export default { externalAuthProviders: `${API_HOST}/login/getOauth2Clients`, tryExternalAuth: `${API_HOST}/login/externalLogin`, - externalLogout: `/login/oauth2/logout`, login: `${API_HOST}/login`, group: `${API_HOST}/groups`, user: `${API_HOST}/users`, From 6f19787dc74a2ad35d33a51fb7a931ce47598837 Mon Sep 17 00:00:00 2001 From: ScottSut Date: Wed, 28 Oct 2020 09:48:39 +0800 Subject: [PATCH 17/18] fix(Display): remove willChange property due to blur --- .../app/containers/Display/components/Layer/Content/LayerBox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/app/containers/Display/components/Layer/Content/LayerBox.tsx b/webapp/app/containers/Display/components/Layer/Content/LayerBox.tsx index 08beddf9c..9582ddbef 100644 --- a/webapp/app/containers/Display/components/Layer/Content/LayerBox.tsx +++ b/webapp/app/containers/Display/components/Layer/Content/LayerBox.tsx @@ -89,7 +89,6 @@ const LayerBox: React.FC = (props: ILayerBoxProps) => { const style: React.CSSProperties = { transform: `translate(${positionX}px, ${positionY}px)`, - willChange: 'transform', width: `${width}px`, height: `${height}px`, zIndex: index, From 6e31d9234fec384b5f50735f0d856afae679010c Mon Sep 17 00:00:00 2001 From: ruanhan1988 <2856197796@qq.com> Date: Wed, 28 Oct 2020 14:09:23 +0800 Subject: [PATCH 18/18] fix(drill) #1971 --- .../DataDrill/abstract/widgetOperating.ts | 7 +- webapp/app/containers/Dashboard/sagas.ts | 79 +++++++++++-------- webapp/share/containers/Dashboard/sagas.ts | 14 +++- 3 files changed, 60 insertions(+), 40 deletions(-) diff --git a/webapp/app/components/DataDrill/abstract/widgetOperating.ts b/webapp/app/components/DataDrill/abstract/widgetOperating.ts index 95cfade15..55294a8ef 100644 --- a/webapp/app/components/DataDrill/abstract/widgetOperating.ts +++ b/webapp/app/components/DataDrill/abstract/widgetOperating.ts @@ -74,7 +74,7 @@ export default class OperatingWidget extends OperateObjectAbstract { } public initGroups() { - const widget = this.getWidgetById(this.currentWidgetId) + let widget = this.getWidgetById(this.currentWidgetId) if (!widget.initGroups) { const { rows, cols, color, label } = widget const setDefaultEmptyArray = setDefaultReplaceNull((f) => f, []) @@ -94,7 +94,10 @@ export default class OperatingWidget extends OperateObjectAbstract { setDefaultEmptyArray )(label) ] - widget.initGroups = groups + widget = { + ...widget, + initGroups: groups + } return groups } return widget.initGroups diff --git a/webapp/app/containers/Dashboard/sagas.ts b/webapp/app/containers/Dashboard/sagas.ts index be18bf006..d175d12ff 100644 --- a/webapp/app/containers/Dashboard/sagas.ts +++ b/webapp/app/containers/Dashboard/sagas.ts @@ -103,17 +103,14 @@ export function* getDashboardDetail(action: DashboardActionType) { operationWidgetProps.widgetIntoPool(widgets) - const formedViews: IFormedViews = views.reduce( - (obj, view) => { - obj[view.id] = { - ...view, - model: JSON.parse(view.model || '{}'), - variable: JSON.parse(view.variable || '[]') - } - return obj - }, - {} - ) + const formedViews: IFormedViews = views.reduce((obj, view) => { + obj[view.id] = { + ...view, + model: JSON.parse(view.model || '{}'), + variable: JSON.parse(view.variable || '[]') + } + return obj + }, {}) yield put(dashboardDetailLoaded(dashboard, items, widgets, formedViews)) } catch (err) { @@ -484,19 +481,26 @@ export function* getDashboardShareLink(action: DashboardActionType) { loadDashboardShareLinkFail } = DashboardActions - const {id, mode, permission, expired, roles, viewers} = action.payload.params + const { + id, + mode, + permission, + expired, + roles, + viewers + } = action.payload.params let requestData = null - switch(mode) { + switch (mode) { case 'AUTH': - requestData = { mode, expired, permission, roles, viewers } - break - case 'PASSWORD': - case 'NORMAL': - requestData = { mode, expired } - break - default: - break + requestData = { mode, expired, permission, roles, viewers } + break + case 'PASSWORD': + case 'NORMAL': + requestData = { mode, expired } + break + default: + break } try { @@ -506,7 +510,7 @@ export function* getDashboardShareLink(action: DashboardActionType) { data: requestData }) - const { token, password} = result.payload + const { token, password } = result.payload switch (mode) { case 'AUTH': yield put(dashboardAuthorizedShareLinkLoaded(token)) @@ -520,14 +524,13 @@ export function* getDashboardShareLink(action: DashboardActionType) { default: break } - } catch (err) { yield put(loadDashboardShareLinkFail()) errorHandler(err) } } -export function* getWidgetShareLink (action: DashboardActionType) { +export function* getWidgetShareLink(action: DashboardActionType) { if (action.type !== ActionTypes.LOAD_WIDGET_SHARE_LINK) { return } @@ -537,19 +540,27 @@ export function* getWidgetShareLink (action: DashboardActionType) { widgetShareLinkLoaded, loadWidgetShareLinkFail } = DashboardActions - const {id, itemId, mode, expired, permission, roles, viewers} = action.payload.params + const { + id, + itemId, + mode, + expired, + permission, + roles, + viewers + } = action.payload.params let requestData = null - switch(mode) { + switch (mode) { case 'AUTH': - requestData = { mode, expired, permission, roles, viewers } - break - case 'PASSWORD': - case 'NORMAL': - requestData = { mode, expired } - break - default: - break + requestData = { mode, expired, permission, roles, viewers } + break + case 'PASSWORD': + case 'NORMAL': + requestData = { mode, expired } + break + default: + break } try { diff --git a/webapp/share/containers/Dashboard/sagas.ts b/webapp/share/containers/Dashboard/sagas.ts index e975b0234..56e111633 100644 --- a/webapp/share/containers/Dashboard/sagas.ts +++ b/webapp/share/containers/Dashboard/sagas.ts @@ -27,13 +27,16 @@ import { takeLatest, takeEvery } from 'redux-saga/effects' + +import { IWidgetFormed } from 'app/containers/Widget/types' import { ActionTypes } from './constants' import { DashboardActions, DashboardActionType } from './actions' import { makeSelectDashboard, makeSelectItemRelatedWidget, makeSelectItemInfo, - makeSelectFormedViews + makeSelectFormedViews, + makeSelectWidgets } from './selectors' import { makeSelectShareType } from 'share/containers/App/selectors' import { @@ -64,7 +67,6 @@ import { IDashboard, IQueryConditions } from 'app/containers/Dashboard/types' -import { IWidgetFormed } from 'app/containers/Widget/types' import { IShareFormedViews } from 'app/containers/View/types' import { IGlobalControlConditions, @@ -82,7 +84,7 @@ import api from 'utils/api' import { message } from 'antd' import { DownloadTypes } from 'app/containers/App/constants' import { localStorageCRUD, getPasswordUrl } from '../../util' - +import { operationWidgetProps } from 'app/components/DataDrill/abstract/widgetOperating' export function* getDashboard(action: DashboardActionType) { if (action.type !== ActionTypes.LOAD_SHARE_DASHBOARD) { return @@ -110,6 +112,7 @@ export function* getDashboard(action: DashboardActionType) { ...rest, config: dashboardConfigMigrationRecorder(parsedConfig) } + const formedWidgets = widgets.map((widget) => { const { config, ...rest } = widget const parsedConfig: IWidgetConfig = JSON.parse(config) @@ -132,6 +135,8 @@ export function* getDashboard(action: DashboardActionType) { {} ) yield put(dashboardGetted(dashboard, relations, formedWidgets, formedViews)) + const getWidgets: IWidgetFormed = yield select(makeSelectWidgets()) + operationWidgetProps.widgetIntoPool(getWidgets) } catch (err) { yield put(loadDashboardFail()) errorHandler(err) @@ -171,7 +176,8 @@ export function* getWidget(action: DashboardActionType) { {} ) yield put(widgetGetted(formedWidget, formedViews)) - + const getWidgets: IWidgetFormed = yield select(makeSelectWidgets()) + operationWidgetProps.widgetIntoPool(getWidgets) if (resolve) { resolve(formedWidget, formedViews) }