From 5a287fff1df02c5136217aa4f7074c3564655275 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 10 Jun 2022 16:57:41 -0300 Subject: [PATCH 01/36] Chore: Add auto label and improve Kodiak configuration (#25829) ## Proposed changes (including videos or screenshots) ## Issue(s) ## Steps to test or reproduce ## Further comments --- .github/auto-label-action-config.json | 1 + .github/workflows/auto-label.yml | 11 +++++++++++ .kodiak.toml | 7 +++++-- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .github/auto-label-action-config.json create mode 100644 .github/workflows/auto-label.yml diff --git a/.github/auto-label-action-config.json b/.github/auto-label-action-config.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/.github/auto-label-action-config.json @@ -0,0 +1 @@ +{} diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000000000..2a453ccdb4d25 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,11 @@ +name: 'Auto label QA' +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: ggazzo/gh-action-auto-label@beta-5 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.kodiak.toml b/.kodiak.toml index bbbfca713a5ea..3865c8954586f 100644 --- a/.kodiak.toml +++ b/.kodiak.toml @@ -1,10 +1,13 @@ # .kodiak.toml version = 1 - [merge] method = "squash" -automerge_label = ["stat: ready to merge", "QA tested", "automerge"] +automerge_label = ["stat: ready to merge", "automerge"] +block_on_neutral_required_check_runs = true +blocking_labels = ["stat: needs QA", "Invalid PR Title"] +prioritize_ready_to_merge = true + [merge.message] title = "pull_request_title" # default: "github_default" From e3d184bc4e6d6c7bd44fde4dfbf75f798a8ea52c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 10 Jun 2022 17:41:55 -0300 Subject: [PATCH 02/36] Regression: Fix users.create call (#25834) ## Proposed changes (including videos or screenshots) ## Issue(s) ## Steps to test or reproduce ## Further comments --- apps/meteor/client/views/admin/users/AddUser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AddUser.js b/apps/meteor/client/views/admin/users/AddUser.js index 5aa3eb2cf9c9d..79e523cee7f84 100644 --- a/apps/meteor/client/views/admin/users/AddUser.js +++ b/apps/meteor/client/views/admin/users/AddUser.js @@ -77,8 +77,8 @@ export function AddUser({ roles, onReload, ...props }) { [router], ); - const saveAction = useEndpointAction('POST', 'users.create', values, t('User_created_successfully!')); - const eventStats = useEndpointAction('POST', 'statistics.telemetry', { + const saveAction = useEndpointAction('POST', '/v1/users.create', values, t('User_created_successfully!')); + const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry', { params: [{ eventName: 'updateCounter', settingsId: 'Manual_Entry_User_Count' }], }); From e01c8eac92467fd1e4e6a21c19539e2950d4f8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Fri, 10 Jun 2022 19:18:50 -0300 Subject: [PATCH 03/36] Chore: Convert MemoizedSetting, Setting, Section (#25572) ## Proposed changes (including videos or screenshots) ## Issue(s) ## Steps to test or reproduce ## Further comments Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .../client/components/Sidebar/Header.tsx | 4 +- .../contexts/TranslationContextMock.tsx | 4 +- .../views/admin/EditableSettingsContext.ts | 20 +++--- .../settings/EditableSettingsProvider.tsx | 12 ++-- .../client/views/admin/settings/GroupPage.tsx | 4 +- ...MemoizedSetting.js => MemoizedSetting.tsx} | 67 +++++++++++++------ .../settings/{Section.js => Section.tsx} | 54 +++++++++++---- .../settings/{Setting.js => Setting.tsx} | 53 +++++++++------ .../settings/inputs/BooleanSettingInput.tsx | 4 +- .../ui-contexts/src/TranslationContext.ts | 4 +- 10 files changed, 144 insertions(+), 82 deletions(-) rename apps/meteor/client/views/admin/settings/{MemoizedSetting.js => MemoizedSetting.tsx} (54%) rename apps/meteor/client/views/admin/settings/{Section.js => Section.tsx} (57%) rename apps/meteor/client/views/admin/settings/{Setting.js => Setting.tsx} (52%) diff --git a/apps/meteor/client/components/Sidebar/Header.tsx b/apps/meteor/client/components/Sidebar/Header.tsx index 2bc7e5cfb4809..278f474015871 100644 --- a/apps/meteor/client/components/Sidebar/Header.tsx +++ b/apps/meteor/client/components/Sidebar/Header.tsx @@ -1,8 +1,8 @@ import { Box, ActionButton } from '@rocket.chat/fuselage'; -import React, { FC, ReactElement } from 'react'; +import React, { FC, ReactNode } from 'react'; type HeaderProps = { - title?: ReactElement | string; + title?: ReactNode; onClose?: () => void; }; diff --git a/apps/meteor/client/stories/contexts/TranslationContextMock.tsx b/apps/meteor/client/stories/contexts/TranslationContextMock.tsx index d4ffd14591548..059f3147dba6e 100644 --- a/apps/meteor/client/stories/contexts/TranslationContextMock.tsx +++ b/apps/meteor/client/stories/contexts/TranslationContextMock.tsx @@ -1,4 +1,4 @@ -import { TranslationContext } from '@rocket.chat/ui-contexts'; +import { TranslationContext, TranslationKey } from '@rocket.chat/ui-contexts'; import i18next from 'i18next'; import React, { ContextType, ReactElement, ReactNode, useContext, useMemo } from 'react'; @@ -41,7 +41,7 @@ const TranslationContextMock = ({ children }: TranslationContextMockProps): Reac }); }; - translate.has = (key: string): boolean => !!key && i18next.exists(key); + translate.has = (key: string | undefined): key is TranslationKey => !!key && i18next.exists(key); return { ...parent, diff --git a/apps/meteor/client/views/admin/EditableSettingsContext.ts b/apps/meteor/client/views/admin/EditableSettingsContext.ts index af21e8d7f4d4d..2c74b679fc9eb 100644 --- a/apps/meteor/client/views/admin/EditableSettingsContext.ts +++ b/apps/meteor/client/views/admin/EditableSettingsContext.ts @@ -1,24 +1,24 @@ -import { ISettingBase, SectionName, SettingId, GroupId, TabId } from '@rocket.chat/core-typings'; +import { ISettingBase, SectionName, SettingId, GroupId, TabId, ISettingColor } from '@rocket.chat/core-typings'; import { SettingsContextQuery } from '@rocket.chat/ui-contexts'; import { createContext, useContext, useMemo } from 'react'; import { useSubscription, Subscription, Unsubscribe } from 'use-subscription'; -export interface IEditableSetting extends ISettingBase { +export type EditableSetting = (ISettingBase | ISettingColor) & { disabled: boolean; changed: boolean; invisible: boolean; -} +}; export type EditableSettingsContextQuery = SettingsContextQuery & { changed?: boolean; }; export type EditableSettingsContextValue = { - readonly queryEditableSetting: (_id: SettingId) => Subscription; - readonly queryEditableSettings: (query: EditableSettingsContextQuery) => Subscription; + readonly queryEditableSetting: (_id: SettingId) => Subscription; + readonly queryEditableSettings: (query: EditableSettingsContextQuery) => Subscription; readonly queryGroupSections: (_id: GroupId, tab?: TabId) => Subscription; readonly queryGroupTabs: (_id: GroupId) => Subscription; - readonly dispatch: (changes: Partial[]) => void; + readonly dispatch: (changes: Partial[]) => void; }; export const EditableSettingsContext = createContext({ @@ -27,7 +27,7 @@ export const EditableSettingsContext = createContext (): void => undefined, }), queryEditableSettings: () => ({ - getCurrentValue: (): IEditableSetting[] => [], + getCurrentValue: (): EditableSetting[] => [], subscribe: (): Unsubscribe => (): void => undefined, }), queryGroupSections: () => ({ @@ -41,14 +41,14 @@ export const EditableSettingsContext = createContext undefined, }); -export const useEditableSetting = (_id: SettingId): IEditableSetting | undefined => { +export const useEditableSetting = (_id: SettingId): EditableSetting | undefined => { const { queryEditableSetting } = useContext(EditableSettingsContext); const subscription = useMemo(() => queryEditableSetting(_id), [queryEditableSetting, _id]); return useSubscription(subscription); }; -export const useEditableSettings = (query?: EditableSettingsContextQuery): IEditableSetting[] => { +export const useEditableSettings = (query?: EditableSettingsContextQuery): EditableSetting[] => { const { queryEditableSettings } = useContext(EditableSettingsContext); const subscription = useMemo(() => queryEditableSettings(query ?? {}), [queryEditableSettings, query]); return useSubscription(subscription); @@ -68,5 +68,5 @@ export const useEditableSettingsGroupTabs = (_id: SettingId): TabId[] => { return useSubscription(subscription); }; -export const useEditableSettingsDispatch = (): ((changes: Partial[]) => void) => +export const useEditableSettingsDispatch = (): ((changes: Partial[]) => void) => useContext(EditableSettingsContext).dispatch; diff --git a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx index 4c0d102baa9fa..4a469f0532180 100644 --- a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx +++ b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx @@ -7,7 +7,7 @@ import { FilterQuery } from 'mongodb'; import React, { useEffect, useMemo, FunctionComponent, useRef, MutableRefObject } from 'react'; import { createReactiveSubscriptionFactory } from '../../../providers/createReactiveSubscriptionFactory'; -import { EditableSettingsContext, IEditableSetting, EditableSettingsContextValue } from '../EditableSettingsContext'; +import { EditableSettingsContext, EditableSetting, EditableSettingsContextValue } from '../EditableSettingsContext'; const defaultQuery: SettingsContextQuery = {}; @@ -16,7 +16,7 @@ type EditableSettingsProviderProps = { }; const EditableSettingsProvider: FunctionComponent = ({ children, query = defaultQuery }) => { - const settingsCollectionRef = useRef>(null) as MutableRefObject>; + const settingsCollectionRef = useRef>(null) as MutableRefObject>; const persistedSettings = useSettings(query); const getSettingsCollection = useMutableCallback(() => { @@ -25,7 +25,7 @@ const EditableSettingsProvider: FunctionComponent } return settingsCollectionRef.current; - }) as () => Mongo.Collection; + }) as () => Mongo.Collection; useEffect(() => { const settingsCollection = getSettingsCollection(); @@ -39,7 +39,7 @@ const EditableSettingsProvider: FunctionComponent const queryEditableSetting = useMemo(() => { const validateSettingQueries = ( query: undefined | string | FilterQuery | FilterQuery[], - settingsCollection: Mongo.Collection, + settingsCollection: Mongo.Collection, ): boolean => { if (!query) { return true; @@ -49,7 +49,7 @@ const EditableSettingsProvider: FunctionComponent return queries.every((query) => settingsCollection.find(query).count() > 0); }; - return createReactiveSubscriptionFactory((_id: SettingId): IEditableSetting | undefined => { + return createReactiveSubscriptionFactory((_id: SettingId): EditableSetting | undefined => { const settingsCollection = getSettingsCollection(); const editableSetting = settingsCollection.findOne(_id); @@ -169,7 +169,7 @@ const EditableSettingsProvider: FunctionComponent [getSettingsCollection], ); - const dispatch = useMutableCallback((changes: Partial[]): void => { + const dispatch = useMutableCallback((changes: Partial[]): void => { for (const { _id, ...data } of changes) { if (!_id) { continue; diff --git a/apps/meteor/client/views/admin/settings/GroupPage.tsx b/apps/meteor/client/views/admin/settings/GroupPage.tsx index 19b16adee851b..20510393b48b3 100644 --- a/apps/meteor/client/views/admin/settings/GroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/GroupPage.tsx @@ -14,7 +14,7 @@ import { import React, { useMemo, memo, FC, ReactNode, FormEvent, MouseEvent } from 'react'; import Page from '../../../components/Page'; -import { useEditableSettingsDispatch, useEditableSettings, IEditableSetting } from '../EditableSettingsContext'; +import { useEditableSettingsDispatch, useEditableSettings, EditableSetting } from '../EditableSettingsContext'; import GroupPageSkeleton from './GroupPageSkeleton'; type GroupPageProps = { @@ -129,7 +129,7 @@ const GroupPage: FC = ({ }; }) .filter(Boolean); - dispatchToEditing(settingsToDispatch as Partial[]); + dispatchToEditing(settingsToDispatch as Partial[]); }); const handleSubmit = (event: FormEvent): void => { diff --git a/apps/meteor/client/views/admin/settings/MemoizedSetting.js b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx similarity index 54% rename from apps/meteor/client/views/admin/settings/MemoizedSetting.js rename to apps/meteor/client/views/admin/settings/MemoizedSetting.tsx index f3b93d838e28e..446958d390d2b 100644 --- a/apps/meteor/client/views/admin/settings/MemoizedSetting.js +++ b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx @@ -1,5 +1,6 @@ +import { ISettingBase, SettingEditor, SettingValue } from '@rocket.chat/core-typings'; import { Callout, Field, Margins } from '@rocket.chat/fuselage'; -import React, { memo } from 'react'; +import React, { ElementType, memo, ReactElement, ReactNode } from 'react'; import ActionSettingInput from './inputs/ActionSettingInput'; import AssetSettingInput from './inputs/AssetSettingInput'; @@ -18,40 +19,62 @@ import SelectSettingInput from './inputs/SelectSettingInput'; import SelectTimezoneSettingInput from './inputs/SelectTimezoneSettingInput'; import StringSettingInput from './inputs/StringSettingInput'; +// @todo: the props are loosely typed because `Setting` needs to typecheck them. +const inputsByType: Record> = { + boolean: BooleanSettingInput, + string: StringSettingInput, + relativeUrl: RelativeUrlSettingInput, + password: PasswordSettingInput, + int: IntSettingInput, + select: SelectSettingInput, + multiSelect: MultiSelectSettingInput, + language: LanguageSettingInput, + color: ColorSettingInput, + font: FontSettingInput, + code: CodeSettingInput, + action: ActionSettingInput, + asset: AssetSettingInput, + roomPick: RoomPickSettingInput, + timezone: SelectTimezoneSettingInput, + date: GenericSettingInput, // @todo: implement + group: GenericSettingInput, // @todo: implement +}; + +type MemoizedSettingProps = { + _id?: string; + type: ISettingBase['type']; + hint?: ReactNode; + callout?: ReactNode; + value?: SettingValue; + editor?: SettingEditor; + onChangeValue?: (value: unknown) => void; + onChangeEditor?: (value: unknown) => void; + onResetButtonClick?: () => void; + className?: string; + invisible?: boolean; + label?: string; + sectionChanged?: boolean; + hasResetButton?: boolean; + actionText?: string; +}; + const MemoizedSetting = ({ type, hint = undefined, callout = undefined, value = undefined, editor = undefined, - onChangeValue = () => {}, - onChangeEditor = () => {}, + onChangeValue, + onChangeEditor, className = undefined, invisible = undefined, ...inputProps -}) => { +}: MemoizedSettingProps): ReactElement | null => { if (invisible) { return null; } - const InputComponent = - { - boolean: BooleanSettingInput, - string: StringSettingInput, - relativeUrl: RelativeUrlSettingInput, - password: PasswordSettingInput, - int: IntSettingInput, - select: SelectSettingInput, - multiSelect: MultiSelectSettingInput, - language: LanguageSettingInput, - color: ColorSettingInput, - font: FontSettingInput, - code: CodeSettingInput, - action: ActionSettingInput, - asset: AssetSettingInput, - roomPick: RoomPickSettingInput, - timezone: SelectTimezoneSettingInput, - }[type] || GenericSettingInput; + const InputComponent = inputsByType[type]; return ( diff --git a/apps/meteor/client/views/admin/settings/Section.js b/apps/meteor/client/views/admin/settings/Section.tsx similarity index 57% rename from apps/meteor/client/views/admin/settings/Section.js rename to apps/meteor/client/views/admin/settings/Section.tsx index e2c4ca138e66d..4649426e10a0a 100644 --- a/apps/meteor/client/views/admin/settings/Section.js +++ b/apps/meteor/client/views/admin/settings/Section.tsx @@ -1,13 +1,24 @@ +import { isSettingColor } from '@rocket.chat/core-typings'; import { Accordion, Box, Button, FieldGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; +import { TranslationKey, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, ReactNode, useMemo } from 'react'; import { useEditableSettings, useEditableSettingsDispatch } from '../EditableSettingsContext'; import SectionSkeleton from './SectionSkeleton'; import Setting from './Setting'; -function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, ...props }) { +type SectionProps = { + groupId: string; + hasReset?: boolean; + sectionName: string; + tabName?: string; + solo: boolean; + help?: ReactNode; + children?: ReactNode; +}; + +function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, help, children }: SectionProps): ReactElement { const editableSettings = useEditableSettings( useMemo( () => ({ @@ -32,26 +43,41 @@ function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, .. dispatch( editableSettings .filter(({ disabled }) => !disabled) - .map(({ _id, value, packageValue, editor, packageEditor }) => ({ - _id, - value: packageValue, - editor: packageEditor, - changed: JSON.stringify(value) !== JSON.stringify(packageValue) || JSON.stringify(editor) !== JSON.stringify(packageEditor), - })), + .map((setting) => { + if (isSettingColor(setting)) { + return { + _id: setting._id, + value: setting.packageValue, + editor: setting.packageEditor, + changed: + JSON.stringify(setting.value) !== JSON.stringify(setting.packageValue) || + JSON.stringify(setting.editor) !== JSON.stringify(setting.packageEditor), + }; + } + return { + _id: setting._id, + value: setting.packageValue, + changed: JSON.stringify(setting.value) !== JSON.stringify(setting.packageValue), + }; + }), ); }); const t = useTranslation(); - const handleResetSectionClick = () => { + const handleResetSectionClick = (): void => { reset(); }; return ( - - {props.help && ( + + {help && ( - {props.help} + {help} )} @@ -60,7 +86,7 @@ function Section({ groupId, hasReset = true, sectionName, tabName = '', solo, .. ))} - {props.children} + {children} {hasReset && canReset && (