From d4dd403e4854e09534d9fbce0694c8063471bae8 Mon Sep 17 00:00:00 2001 From: CuriousCorrelation <58817502+CuriousCorrelation@users.noreply.github.com> Date: Thu, 9 Sep 2021 10:57:15 +0530 Subject: [PATCH 1/9] [GH-1130] Fixes 'Today' button not selecting date and entered date not being selected in calendar (#1149) * [GH-1130] Fix 'Today' button not selecting date * [GH-1130] Fix entered day not selected in calendar * [GH-1130] Add test for calendar Today button Co-authored-by: Harshil Sharma --- .../properties/dateRange/dateRange.test.tsx | 30 +++++++++++++++++++ .../properties/dateRange/dateRange.tsx | 2 ++ 2 files changed, 32 insertions(+) diff --git a/webapp/src/components/properties/dateRange/dateRange.test.tsx b/webapp/src/components/properties/dateRange/dateRange.test.tsx index 0e14fb845..d70a45468 100644 --- a/webapp/src/components/properties/dateRange/dateRange.test.tsx +++ b/webapp/src/components/properties/dateRange/dateRange.test.tsx @@ -242,4 +242,34 @@ describe('components/properties/dateRange', () => { const retVal = '{"from":' + June15.getTime().toString() + ',"to":' + June20.getTime().toString() + '}' expect(callback).toHaveBeenCalledWith(retVal) }) + + test('handles `Today` button click event', () => { + const callback = jest.fn() + const component = wrapIntl( + , + ) + + // To see if 'Today' button correctly selects today's date, + // we can check it against `new Date()`. + // About `Date()` + // > "When called as a function, returns a string representation of the current date and time" + const date = new Date() + const today = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) + + const {getByText, getByTitle} = render(component) + const dayDisplay = getByTitle('Empty') + userEvent.click(dayDisplay) + + const day = getByText('Today') + const modal = getByTitle('Close').children[0] + userEvent.click(day) + userEvent.click(modal) + + const rObject = {from: today} + expect(callback).toHaveBeenCalledWith(JSON.stringify(rObject)) + }) }) diff --git a/webapp/src/components/properties/dateRange/dateRange.tsx b/webapp/src/components/properties/dateRange/dateRange.tsx index 50048a0c2..900e9ce62 100644 --- a/webapp/src/components/properties/dateRange/dateRange.tsx +++ b/webapp/src/components/properties/dateRange/dateRange.tsx @@ -233,6 +233,8 @@ function DateRange(props: Props): JSX.Element { locale={locale} localeUtils={MomentLocaleUtils} todayButton={intl.formatMessage({id: 'DateRange.today', defaultMessage: 'Today'})} + onTodayButtonClick={handleDayClick} + month={dateFrom} selectedDays={[dateFrom, dateTo ? {from: dateFrom, to: dateTo} : {from: dateFrom, to: dateFrom}]} modifiers={dateTo ? {start: dateFrom, end: dateTo} : {start: dateFrom, end: dateFrom}} /> From add57216a774e1cbd4da914ee7e4ddd1a68040c8 Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Thu, 9 Sep 2021 15:35:30 +0530 Subject: [PATCH 2/9] ADded submath support in one missed place (#1156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jesús Espino --- webapp/src/octoClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/octoClient.ts b/webapp/src/octoClient.ts index d629baf43..e0033b13c 100644 --- a/webapp/src/octoClient.ts +++ b/webapp/src/octoClient.ts @@ -86,7 +86,7 @@ class OctoClient { async getClientConfig(): Promise { const path = '/api/v1/clientConfig' - const response = await fetch(this.serverUrl + path, { + const response = await fetch(this.getBaseURL() + path, { method: 'GET', headers: this.headers(), }) From bb4b463a4f464c631b3e76dda74b76b4b72ba4f6 Mon Sep 17 00:00:00 2001 From: Harshil Sharma Date: Thu, 9 Sep 2021 16:18:07 +0530 Subject: [PATCH 3/9] Workspace Switcher - include channels with no workspaces and boards (#1158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * API WIP * WIP * Finished changes * Fixed colors: * Don't enforce charset adn collation in migration, pick from database DSN * Added MySQL query * Updated mocks * Added tests * Lint fixes * Fixed typo and removed unsed style * Checked in a snapshot * Updated snapshot * Updated Cypress test * Updated Cypress test * Updated Cypress test * Fixed review comments * Fixed tests * Added default collation for MySQL * Added documentation for ensuring correct database collation * Updated migrations * Fixed a bug with collation * Fixed lint errors * Used correct collation * debugging * Updating css * Minor UI changes * USe inbuilt default collation * Used only charset for mysql * Fixed linter issue: * Added migration for matching collation * Reverted local config changes * Reverted local config changes * Handled the case of personal server running on MySQL * WIP * Now running collation matching migration onlyt for plugins * Minor optimization * Multiple review fixes * Added group by clause to primary query * Supported for subpacth * Displayed all channels in workspace switcher * Included channels without a Focalbaord workspaces in worksapce switcher as well Co-authored-by: Asaad Mahmood Co-authored-by: Jesús Espino --- server/services/store/sqlstore/workspaces.go | 26 +++++++++++------- .../workspaceSwitcher/workspaceOptions.tsx | 27 ++++++++++++++----- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/server/services/store/sqlstore/workspaces.go b/server/services/store/sqlstore/workspaces.go index 670f0a9ba..da98f1f96 100644 --- a/server/services/store/sqlstore/workspaces.go +++ b/server/services/store/sqlstore/workspaces.go @@ -156,24 +156,30 @@ func (s *SQLStore) GetWorkspaceCount() (int64, error) { func (s *SQLStore) GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) { var query sq.SelectBuilder - query = s.getQueryBuilder(). - Select("Channels.ID", "Channels.DisplayName", "COUNT(focalboard_blocks.id)"). - From("focalboard_blocks"). - Join("ChannelMembers ON focalboard_blocks.workspace_id = ChannelMembers.ChannelId"). - Join("Channels ON ChannelMembers.ChannelId = Channels.Id"). - Where(sq.Eq{"ChannelMembers.UserId": userID}). - Where(sq.Eq{"focalboard_blocks.type": "board"}). - GroupBy("Channels.Id", "Channels.DisplayName") + var nonTemplateFilter string switch s.dbType { case mysqlDBType: - query = query.Where(sq.Like{"focalboard_blocks.fields": "%\"isTemplate\":false%"}) + nonTemplateFilter = "focalboard_blocks.fields LIKE %\"isTemplate\":false%" case postgresDBType: - query = query.Where("focalboard_blocks.fields ->> 'isTemplate' = 'false'") + nonTemplateFilter = "focalboard_blocks.fields ->> 'isTemplate' = 'false'" default: return nil, fmt.Errorf("GetUserWorkspaces - %w", errUnsupportedDatabaseError) } + query = s.getQueryBuilder(). + Select("Channels.ID", "Channels.DisplayName", "COUNT(focalboard_blocks.id)"). + From("ChannelMembers"). + // select channels without a corresponding workspace + LeftJoin( + "focalboard_blocks ON focalboard_blocks.workspace_id = ChannelMembers.ChannelId AND "+ + "focalboard_blocks.type = 'board' AND "+ + nonTemplateFilter, + ). + Join("Channels ON ChannelMembers.ChannelId = Channels.Id"). + Where(sq.Eq{"ChannelMembers.UserId": userID}). + GroupBy("Channels.Id", "Channels.DisplayName") + rows, err := query.Query() if err != nil { s.logger.Error("ERROR GetUserWorkspaces", mlog.Err(err)) diff --git a/webapp/src/components/workspaceSwitcher/workspaceOptions.tsx b/webapp/src/components/workspaceSwitcher/workspaceOptions.tsx index 8431d9636..f71270c8f 100644 --- a/webapp/src/components/workspaceSwitcher/workspaceOptions.tsx +++ b/webapp/src/components/workspaceSwitcher/workspaceOptions.tsx @@ -21,13 +21,26 @@ type Props = { const WorkspaceOptions = (props: Props): JSX.Element => { const intl = useIntl() const userWorkspaces = useAppSelector(getUserWorkspaceList) - const options = userWorkspaces.filter((workspace) => workspace.id !== props.activeWorkspaceId).map((workspace) => { - return { - label: workspace.title, - value: workspace.id, - boardCount: workspace.boardCount, - } - }) + const options = userWorkspaces. + filter((workspace) => workspace.id !== props.activeWorkspaceId). + map((workspace) => { + return { + label: workspace.title, + value: workspace.id, + boardCount: workspace.boardCount, + } + }). + sort((a, b) => { + // This will arrange into two groups - + // on the top we'll have workspaces with boards + // and below that we'll have onces with no boards, + // and each group will be sorted alphabetically within itself. + if ((a.boardCount === 0 && b.boardCount === 0) || (a.boardCount !== 0 && b.boardCount !== 0)) { + return a.label.localeCompare(b.label) + } + + return b.boardCount - a.boardCount + }) return ( + +`; + +exports[`components/propertyValueElement should match snapshot, select 1`] = ` +
+
+ + + value 1 + +
+ +
+
+
+
+`; + +exports[`components/propertyValueElement should match snapshot, select, read-only 1`] = ` +
+
+ + + value 1 + + +
+
+`; + +exports[`components/propertyValueElement should match snapshot, url, array value 1`] = ` +
+ +
+`; + +exports[`components/propertyValueElement should match snapshot, url, array value 2`] = ` +
+ +
+`; diff --git a/webapp/src/components/propertyValueElement.test.tsx b/webapp/src/components/propertyValueElement.test.tsx new file mode 100644 index 000000000..7ed877c2a --- /dev/null +++ b/webapp/src/components/propertyValueElement.test.tsx @@ -0,0 +1,200 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react' +import {render} from '@testing-library/react' +import '@testing-library/jest-dom' +import {IntlProvider} from 'react-intl' + +import 'isomorphic-fetch' +import {DndProvider} from 'react-dnd' +import {HTML5Backend} from 'react-dnd-html5-backend' + +import {IPropertyTemplate, IPropertyOption} from '../blocks/board' + +import {TestBlockFactory} from '../test/testBlockFactory' + +import PropertyValueElement from './propertyValueElement' + +const wrapProviders = (children: any) => { + return ( + + {children} + + ) +} + +describe('components/propertyValueElement', () => { + const board = TestBlockFactory.createBoard() + const card = TestBlockFactory.createCard(board) + const comments = TestBlockFactory.createComment(card) + + test('should match snapshot, select', async () => { + const propertyTemplate = board.fields.cardProperties.find((p) => p.id === 'property1') + const component = wrapProviders( + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('should match snapshot, select, read-only', async () => { + const propertyTemplate = board.fields.cardProperties.find((p) => p.id === 'property1') + const component = wrapProviders( + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('should match snapshot, multi-select', () => { + const options: IPropertyOption[] = [] + for (let i = 0; i < 3; i++) { + const propertyOption: IPropertyOption = { + id: `ms${i}`, + value: `value ${i}`, + color: 'propColorBrown', + } + options.push(propertyOption) + } + + const propertyTemplate: IPropertyTemplate = { + id: 'multiSelect', + name: 'MultiSelect', + type: 'multiSelect', + options, + } + card.fields.properties.multiSelect = ['ms1', 'ms2'] + const component = wrapProviders( + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('should match snapshot, url, array value', () => { + const propertyTemplate: IPropertyTemplate = { + id: 'property_url', + name: 'Property URL', + type: 'url', + options: [], + } + card.fields.properties.property_url = ['http://localhost'] + + const component = wrapProviders( + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('should match snapshot, url, array value', () => { + const propertyTemplate: IPropertyTemplate = { + id: 'property_url', + name: 'Property URL', + type: 'url', + options: [], + } + card.fields.properties.property_url = ['http://localhost'] + + const component = wrapProviders( + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('should match snapshot, person, array value', () => { + const propertyTemplate: IPropertyTemplate = { + id: 'text', + name: 'Generic Text', + type: 'text', + options: [], + } + card.fields.properties.person = ['value1', 'value2'] + + const component = wrapProviders( + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('should match snapshot, date, array value', () => { + const propertyTemplate: IPropertyTemplate = { + id: 'date', + name: 'Date', + type: 'date', + options: [], + } + card.fields.properties.date = ['invalid date'] + + const component = wrapProviders( + , + ) + const {container} = render(component) + expect(container).toMatchSnapshot() + }) +}) diff --git a/webapp/src/components/propertyValueElement.tsx b/webapp/src/components/propertyValueElement.tsx index 48929d161..622590e53 100644 --- a/webapp/src/components/propertyValueElement.tsx +++ b/webapp/src/components/propertyValueElement.tsx @@ -188,7 +188,7 @@ const PropertyValueElement = (props:Props): JSX.Element => { } else if (propertyTemplate.type === 'person') { return ( mutator.changePropertyValue(card, propertyTemplate.id, newValue)} /> @@ -200,14 +200,14 @@ const PropertyValueElement = (props:Props): JSX.Element => { return ( mutator.changePropertyValue(card, propertyTemplate.id, newValue)} /> ) } else if (propertyTemplate.type === 'url') { return ( { setValue(propertyValue)}