diff --git a/codegen.ts b/codegen.ts new file mode 100644 index 0000000000..2f9d12ab25 --- /dev/null +++ b/codegen.ts @@ -0,0 +1,26 @@ +import type { CodegenConfig } from "@graphql-codegen/cli"; + +const config: CodegenConfig = { + schema: "sdlschema/**/*.graphql", + documents: ["./src/**/*.ts", "./src/**/*.graphql", "./src/**/*.gql"], + hooks: { + afterAllFileWrite: ["prettier --write"], + }, + overwrite: true, + generates: { + "./src/gql/generated/types.ts": { + plugins: ["typescript", "typescript-operations"], + config: { + preResolveTypes: true, + arrayInputCoercion: false, + scalars: { + StringMap: "{ [key: string]: any }", + Time: "Date", + Duration: "number", + }, + }, + }, + }, +}; + +export default config; diff --git a/codegen.yml b/codegen.yml deleted file mode 100644 index 610c4f5dee..0000000000 --- a/codegen.yml +++ /dev/null @@ -1,21 +0,0 @@ - schema: sdlschema/**/*.graphql - documents: - - ./src/**/*.ts - - ./src/**/*.graphql - - ./src/**/*.gql - hooks: - afterAllFileWrite: - - prettier --write - overwrite: true - generates: - ./src/gql/generated/types.ts: - plugins: - - typescript - - typescript-operations - config: - preResolveTypes: true, - arrayInputCoercion: false - scalars: - StringMap: "{ [key: string]: any }" - Time: Date - Duration: number diff --git a/cypress/integration/host/host_events.ts b/cypress/integration/host/host_events.ts index a2010ae5f0..2ccd0a4d14 100644 --- a/cypress/integration/host/host_events.ts +++ b/cypress/integration/host/host_events.ts @@ -93,15 +93,19 @@ describe("Host events", () => { }, { hostType: "host-running-task-set", - text: "Assigned to run task evergreen_ubuntu1604_test_command_patch_5e823e1f28...", + text: "Assigned to run task evergreen_ubuntu1604_test_command_patch_5e823e1f28", }, { hostType: "host-running-task-cleared", - text: "Current running task cleared (was:evergreen_ubuntu1604_test_command_patch_5e823e1f28...)", + text: "Current running task cleared (was: ", + }, + { + hostType: "host-running-task-cleared", + text: "evergreen_ubuntu1604_test_command_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48", }, { hostType: "host-task-finished", - text: "Task evergreen_ubuntu1604_test_command_patch_5e823e1f28... completed with status: test-timed-out", + text: "Task evergreen_ubuntu1604_test_command_patch_5e823e1f28baeaa22ae00823d83e03082cd148ab_5e4ff3abe3c3317e352062e4_20_02_21_15_13_48 completed with status: test-timed-out", }, ]; hostTypes.forEach(({ hostType, text }) => { diff --git a/cypress/integration/preferences/notifications.ts b/cypress/integration/preferences/notifications.ts index 51c077f32a..26215be3cc 100644 --- a/cypress/integration/preferences/notifications.ts +++ b/cypress/integration/preferences/notifications.ts @@ -24,17 +24,65 @@ describe("global subscription settings", () => { }); describe("user subscriptions table", () => { - it("shows all of a user's subscriptions and expands with details", () => { + beforeEach(() => { cy.visit(pageRoute); + }); + + it("shows all of a user's subscriptions and expands with details", () => { cy.dataCy("subscription-row").should("have.length", 3); cy.dataCy("regex-selectors").should("not.be.visible"); cy.dataCy("trigger-data").should("not.be.visible"); - cy.get("tr button").first().click(); + cy.dataCy("subscription-row") + .eq(0) + .within(() => { + cy.get("button").first().click(); + }); cy.dataCy("regex-selectors").should("be.visible"); cy.dataCy("trigger-data").should("not.be.visible"); - cy.get("tr button").last().click(); + cy.dataCy("subscription-row") + .eq(2) + .within(() => { + cy.get("button").first().click(); + }); cy.dataCy("regex-selectors").should("be.visible"); cy.dataCy("trigger-data").should("be.visible"); }); + + it("Shows the selected count in the 'Delete' button", () => { + cy.dataCy("subscription-row") + .eq(0) + .within(() => { + cy.get("input[type=checkbox]").check({ force: true }); + }); + cy.dataCy("delete-some-button").contains("Delete (1)"); + + cy.get("thead").within(() => { + cy.get("input[type=checkbox]").check({ force: true }); + }); + cy.dataCy("delete-some-button").contains("Delete (3)"); + + cy.get("thead").within(() => { + cy.get("input[type=checkbox]").uncheck({ force: true }); + }); + cy.dataCy("delete-some-button").contains("Delete"); + cy.dataCy("delete-some-button").should( + "have.attr", + "aria-disabled", + "true" + ); + }); + + describe("Deleting subscriptions", { testIsolation: false }, () => { + it("Deletes a single subscription", () => { + cy.dataCy("subscription-row") + .eq(0) + .within(() => { + cy.get("input[type=checkbox]").check({ force: true }); + }); + cy.dataCy("delete-some-button").click(); + cy.validateToast("success", "Deleted 1 subscription."); + cy.dataCy("subscription-row").should("have.length", 2); + }); + }); }); diff --git a/cypress/integration/project/patches.ts b/cypress/integration/project/patches.ts index 3cd61a5c93..ba06d0bd2a 100644 --- a/cypress/integration/project/patches.ts +++ b/cypress/integration/project/patches.ts @@ -15,4 +15,11 @@ describe("Project Patches Page", () => { cy.location("pathname").should("eq", adminPatchesRoute); cy.dataCy("patch-card").should("exist"); }); + + it("Project dropdown navigates to another project patches page upon selection", () => { + cy.visit(evergreenPatchesRoute); + cy.dataCy("project-select").click(); + cy.dataCy("project-display-name").contains("Spruce").click(); + cy.location("pathname").should("eq", "/project/spruce/patches"); + }); }); diff --git a/cypress/integration/projectSettings/constants.ts b/cypress/integration/projectSettings/constants.ts index cdc8eea829..ed9c8938ee 100644 --- a/cypress/integration/projectSettings/constants.ts +++ b/cypress/integration/projectSettings/constants.ts @@ -19,6 +19,27 @@ export const getPluginsRoute = (identifier: string) => export const getContainersRoute = (identifier: string) => `${getSettingsRoute(identifier)}/containers`; +export const getViewsAndFiltersRoute = (identifier: string) => + `${getSettingsRoute(identifier)}/views-and-filters`; + export const project = "spruce"; export const projectUseRepoEnabled = "evergreen"; export const repo = "602d70a2b2373672ee493184"; + +/** + * `saveButtonEnabled` checks if the save button is enabled or disabled. + * @param isEnabled - if true, the save button should be enabled. If false, the save button should be disabled. + */ +export const saveButtonEnabled = (isEnabled: boolean = true) => { + cy.dataCy("save-settings-button").should( + isEnabled ? "not.have.attr" : "have.attr", + "aria-disabled", + "true" + ); +}; + +export const clickSave = () => { + cy.dataCy("save-settings-button") + .should("not.have.attr", "aria-disabled", "true") + .click(); +}; diff --git a/cypress/integration/projectSettings/project_settings.ts b/cypress/integration/projectSettings/project_settings.ts index 5a2b59b2ff..3133cdb623 100644 --- a/cypress/integration/projectSettings/project_settings.ts +++ b/cypress/integration/projectSettings/project_settings.ts @@ -8,6 +8,8 @@ import { project, projectUseRepoEnabled, repo, + saveButtonEnabled, + clickSave, } from "./constants"; describe("Access page", { testIsolation: false }, () => { @@ -603,9 +605,12 @@ describe( cy.dataCy("display-name-input").should("not.have.attr", "placeholder"); }); - it.skip("Shows a navigation warning modal when navigating away from project settings", () => { + it("Shows a navigation warning modal that lists the general page when navigating away from project settings", () => { cy.contains("My Patches").click(); cy.dataCy("navigation-warning-modal").should("be.visible"); + cy.dataCy("unsaved-pages").within(() => { + cy.get("li").should("have.length", 1); + }); cy.get("body").type("{esc}"); }); @@ -1053,9 +1058,9 @@ describe("Notifications", { testIsolation: false }, () => { cy.selectLGOption("Event", "Any Task Finishes"); cy.selectLGOption("Notification Method", "Comment on a JIRA issue"); cy.getInputByLabel("JIRA Issue").type("JIRA-123"); - cy.contains( - "JIRA comment subscription not allowed for tasks in a project" - ).should("exist"); + cy.contains("Subscription type not allowed for tasks in a project.").should( + "exist" + ); cy.dataCy("save-settings-button").scrollIntoView(); saveButtonEnabled(false); }); @@ -1205,21 +1210,3 @@ describe("Containers", () => { cy.validateToast("success", "Successfully updated project"); }); }); - -/** - * `saveButtonEnabled` checks if the save button is enabled or disabled. - * @param isEnabled - if true, the save button should be enabled. If false, the save button should be disabled. - */ -const saveButtonEnabled = (isEnabled: boolean = true) => { - cy.dataCy("save-settings-button").should( - isEnabled ? "not.have.attr" : "have.attr", - "aria-disabled", - "true" - ); -}; - -const clickSave = () => { - cy.dataCy("save-settings-button") - .should("not.have.attr", "aria-disabled", "true") - .click(); -}; diff --git a/cypress/integration/projectSettings/views_and_filters.ts b/cypress/integration/projectSettings/views_and_filters.ts new file mode 100644 index 0000000000..f01f2f7820 --- /dev/null +++ b/cypress/integration/projectSettings/views_and_filters.ts @@ -0,0 +1,61 @@ +import { + getViewsAndFiltersRoute, + saveButtonEnabled, + clickSave, +} from "./constants"; + +describe("Views & filters page", () => { + const destination = getViewsAndFiltersRoute("sys-perf"); + + beforeEach(() => { + cy.visit(destination); + // Wait for page content to finish loading. + cy.dataCy("parsley-filter-list").children().should("have.length", 2); + saveButtonEnabled(false); + }); + + describe("parsley filters", () => { + it("does not allow saving with invalid regular expression or empty expression", () => { + cy.contains("button", "Add filter").should("be.visible").click(); + cy.dataCy("parsley-filter-expression").first().type("*"); + saveButtonEnabled(false); + cy.contains("Value should be a valid regex expression."); + cy.dataCy("parsley-filter-expression").first().clear(); + saveButtonEnabled(false); + }); + + it("does not allow saving with duplicate filter expressions", () => { + cy.contains("button", "Add filter").should("be.visible").click(); + cy.dataCy("parsley-filter-expression").first().type("filter_1"); + saveButtonEnabled(false); + cy.contains("Filter expression already appears in this project."); + }); + + it("can successfully save and delete filter", () => { + cy.contains("button", "Add filter").should("be.visible").click(); + cy.dataCy("parsley-filter-expression").first().type("my_filter"); + saveButtonEnabled(true); + clickSave(); + cy.validateToast("success", "Successfully updated project"); + cy.dataCy("parsley-filter-list").children().should("have.length", 3); + + cy.dataCy("delete-item-button").first().should("be.visible").click(); + clickSave(); + cy.validateToast("success", "Successfully updated project"); + cy.dataCy("parsley-filter-list").children().should("have.length", 2); + }); + }); + + describe("project view", () => { + it("updates field to 'all' view and back to 'default'", () => { + cy.getInputByLabel("All tasks view").click({ force: true }); + clickSave(); + cy.validateToast("success", "Successfully updated project"); + cy.getInputByLabel("All tasks view").should("be.checked"); + + cy.getInputByLabel("Default view").click({ force: true }); + clickSave(); + cy.validateToast("success", "Successfully updated project"); + }); + }); +}); diff --git a/cypress/integration/version/name_change_modal.ts b/cypress/integration/version/name_change_modal.ts new file mode 100644 index 0000000000..4b372e95b8 --- /dev/null +++ b/cypress/integration/version/name_change_modal.ts @@ -0,0 +1,37 @@ +describe("Name change modal", () => { + beforeEach(() => { + cy.visit("version/5f74d99ab2373627c047c5e5"); + }); + + it("Use the name change modal to change the name of a patch", () => { + const originalName = "main: EVG-7823 add a commit queue message (#4048)"; + cy.contains(originalName); + cy.dataCy("name-change-modal-trigger").click(); + const newName = "a different name"; + cy.get("textarea").clear().type(newName); + cy.contains("Confirm").click(); + cy.get("textarea").should("not.exist"); + cy.contains(newName); + cy.validateToast("success", "Patch name was successfully updated.", true); + // revert name change + cy.dataCy("name-change-modal-trigger").click(); + cy.get("textarea").clear().type(originalName); + cy.contains("Confirm").click(); + cy.get("textarea").should("not.exist"); + cy.validateToast("success", "Patch name was successfully updated.", true); + cy.contains(originalName); + }); + + it("The confirm button is disabled when the text area value is empty or greater than 300 characters", () => { + cy.dataCy("name-change-modal-trigger").click(); + cy.get("textarea").clear(); + cy.contains("button", "Confirm").should("be.disabled"); + cy.get("textarea").type("lol"); + cy.contains("button", "Confirm").should("not.be.disabled"); + const over300Chars = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + cy.get("textarea").type(over300Chars); + cy.contains("button", "Confirm").should("be.disabled"); + cy.contains("should NOT be longer than 300 characters"); + }); +}); diff --git a/cypress/integration/version/restart_modal.ts b/cypress/integration/version/restart_modal.ts index 6c58f1bf69..08d56e2da8 100644 --- a/cypress/integration/version/restart_modal.ts +++ b/cypress/integration/version/restart_modal.ts @@ -37,7 +37,7 @@ describe("Restarting a patch", { testIsolation: false }, () => { cy.dataCy("task-status-badge").should("contain.text", "1 of 1 Selected"); }); - it("Selecting on the patch status filter should toggle the tasks that have matching statuses to it", () => { + it("Selecting on the task status filter should toggle the tasks that have matching statuses to it", () => { cy.dataCy("task-status-filter").click(); cy.getInputByLabel("All").check({ force: true }); cy.dataCy("task-status-filter").click(); diff --git a/package.json b/package.json index 2667df1548..93b3874271 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.83", + "version": "3.0.89", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", @@ -11,7 +11,7 @@ "build:staging": "env-cmd -e staging yarn build", "build": "GIT_SHA=`git rev-parse HEAD` vite build", "check-types": "tsc -p tsconfig.json --noEmit", - "codegen": "graphql-codegen --config codegen.yml", + "codegen": "graphql-codegen --config codegen.ts", "cy:open": "cypress open", "cy:run": "cypress run", "cy:test": "cypress run --spec", @@ -53,12 +53,11 @@ }, "dependencies": { "@apollo/client": "3.6.9", - "@bugsnag/js": "7.18.0", + "@bugsnag/js": "7.20.2", "@bugsnag/plugin-react": "7.18.0", - "@emotion/css": "^11.9.0", - "@emotion/eslint-plugin": "11.0.0", - "@emotion/react": "11.1.4", - "@emotion/styled": "11.0.0", + "@emotion/css": "11.11.0", + "@emotion/react": "11.11.0", + "@emotion/styled": "11.11.0", "@leafygreen-ui/badge": "8.0.2", "@leafygreen-ui/banner": "7.0.4", "@leafygreen-ui/button": "20.0.0", @@ -80,6 +79,7 @@ "@leafygreen-ui/menu": "20.0.0", "@leafygreen-ui/modal": "14.0.1", "@leafygreen-ui/number-input": "1.0.4", + "@leafygreen-ui/pagination": "1.0.7", "@leafygreen-ui/palette": "3.4.4", "@leafygreen-ui/popover": "11.0.1", "@leafygreen-ui/radio-box-group": "12.0.1", @@ -95,9 +95,9 @@ "@leafygreen-ui/text-input": "12.1.0", "@leafygreen-ui/toast": "6.1.4", "@leafygreen-ui/toggle": "10.0.1", - "@leafygreen-ui/tokens": "1.4.1", + "@leafygreen-ui/tokens": "2.1.0", "@leafygreen-ui/tooltip": "9.1.6", - "@leafygreen-ui/typography": "15.1.0", + "@leafygreen-ui/typography": "16.4.1", "@rjsf/core": "4.2.0", "ansi_up": "5.1.0", "antd": "4.20.0", @@ -127,15 +127,15 @@ "react-window-infinite-loader": "^1.0.8" }, "devDependencies": { - "@babel/core": "7.21.8", "@babel/plugin-proposal-private-property-in-object": "^7.17.12", "@babel/preset-react": "^7.12.13", "@bugsnag/source-maps": "^2.3.0", - "@emotion/babel-plugin": "^11.1.2", - "@emotion/jest": "^11.9.1", - "@graphql-codegen/cli": "3.1.0", - "@graphql-codegen/typescript": "3.0.1", - "@graphql-codegen/typescript-operations": "3.0.1", + "@emotion/babel-plugin": "11.11.0", + "@emotion/eslint-plugin": "11.11.0", + "@emotion/jest": "11.11.0", + "@graphql-codegen/cli": "3.2.2", + "@graphql-codegen/typescript": "3.0.2", + "@graphql-codegen/typescript-operations": "3.0.2", "@graphql-eslint/eslint-plugin": "3.18.0", "@originjs/vite-plugin-commonjs": "1.0.3", "@storybook/addon-actions": "7.0.0-rc.5", @@ -203,7 +203,7 @@ "serve-handler": "^6.1.5", "storybook": "7.0.0-rc.5", "typescript": "5.0.3", - "vite": "4.3.7", + "vite": "4.3.9", "vite-plugin-checker": "0.6.0", "vite-plugin-env-compatible": "1.1.1", "vite-plugin-imp": "2.4.0", diff --git a/src/analytics/patch/usePatchAnalytics.ts b/src/analytics/patch/usePatchAnalytics.ts index 7ed652d766..b5c61bee82 100644 --- a/src/analytics/patch/usePatchAnalytics.ts +++ b/src/analytics/patch/usePatchAnalytics.ts @@ -45,6 +45,7 @@ type Action = subscription: SaveSubscriptionForUserMutationVariables["subscription"]; } | { name: "Toggle Display Task Dropdown"; expanded: boolean } + | { name: "Set Patch Visibility"; hidden: boolean } | { name: "Click Base Commit Link" } | { name: "Open Schedule Tasks Modal" }; diff --git a/src/analytics/patches/useProjectPatchesAnalytics.ts b/src/analytics/patches/useProjectPatchesAnalytics.ts index f2e351d209..7bcd2c5e7a 100644 --- a/src/analytics/patches/useProjectPatchesAnalytics.ts +++ b/src/analytics/patches/useProjectPatchesAnalytics.ts @@ -7,11 +7,12 @@ import { import { useGetUserQuery } from "analytics/useGetUserQuery"; type Action = - | { name: "Filter Patches"; filterBy: string } - | { name: "Filter Commit Queue" } | { name: "Change Page Size" } + | { name: "Change Project" } | { name: "Click Patch Link" } - | { name: "Click Variant Icon"; variantIconStatus: string }; + | { name: "Click Variant Icon"; variantIconStatus: string } + | { name: "Filter Commit Queue" } + | { name: "Filter Patches"; filterBy: string }; interface P extends Properties {} interface Analytics extends A {} diff --git a/src/components/Breadcrumbs/__snapshots__/Breadcrumbs.stories.storyshot b/src/components/Breadcrumbs/__snapshots__/Breadcrumbs.stories.storyshot index ae230bd30f..389bd2e41d 100644 --- a/src/components/Breadcrumbs/__snapshots__/Breadcrumbs.stories.storyshot +++ b/src/components/Breadcrumbs/__snapshots__/Breadcrumbs.stories.storyshot @@ -9,7 +9,7 @@ exports[`storybook Storyshots components/Breadcrumbs Default 1`] = ` class="leafygreen-ui-cssveg" > ` font-weight: normal; margin-right: ${size.s}; margin-bottom: ${size.s}; diff --git a/src/components/Dropdown/__snapshots__/Dropdown.stories.storyshot b/src/components/Dropdown/__snapshots__/Dropdown.stories.storyshot index 829ea31088..8de3094d16 100644 --- a/src/components/Dropdown/__snapshots__/Dropdown.stories.storyshot +++ b/src/components/Dropdown/__snapshots__/Dropdown.stories.storyshot @@ -76,7 +76,7 @@ exports[`storybook Storyshots components/Dropdown Default 1`] = ` class="css-soeq2u-LabelWrapper e1yw4j303" >

diff --git a/src/components/EditableTagField/__snapshots__/EditableTagField.stories.storyshot b/src/components/EditableTagField/__snapshots__/EditableTagField.stories.storyshot index 2819bb73f0..287ad8e8ad 100644 --- a/src/components/EditableTagField/__snapshots__/EditableTagField.stories.storyshot +++ b/src/components/EditableTagField/__snapshots__/EditableTagField.stories.storyshot @@ -25,7 +25,7 @@ exports[`storybook Storyshots components/EditableTagField Default 1`] = ` class="leafygreen-ui-a533bt" >

Error

Ouch! That's gotta hurt,
diff --git a/src/components/ExpandedText/__snapshots__/ExpandedText.stories.storyshot b/src/components/ExpandedText/__snapshots__/ExpandedText.stories.storyshot index fbb62686c9..e514cf2b8a 100644 --- a/src/components/ExpandedText/__snapshots__/ExpandedText.stories.storyshot +++ b/src/components/ExpandedText/__snapshots__/ExpandedText.stories.storyshot @@ -3,7 +3,7 @@ exports[`storybook Storyshots components/ExpandedText Default 1`] = `
more diff --git a/src/components/FilterBadges/__snapshots__/FilterBadges.stories.storyshot b/src/components/FilterBadges/__snapshots__/FilterBadges.stories.storyshot index 3763614747..0dcdb4fb17 100644 --- a/src/components/FilterBadges/__snapshots__/FilterBadges.stories.storyshot +++ b/src/components/FilterBadges/__snapshots__/FilterBadges.stories.storyshot @@ -60,7 +60,7 @@ exports[`storybook Storyshots components/FilterBadges Default 1`] = ` class="leafygreen-ui-a533bt" >

5/10 failing tests

@@ -88,7 +88,7 @@ exports[`storybook Storyshots Components/HistoryTable/Icons Active Icons 1`] = `

1/2 failing tests

@@ -119,7 +119,7 @@ exports[`storybook Storyshots Components/HistoryTable/Icons Active Icons 1`] = `

5/10 failing tests

@@ -190,7 +190,7 @@ exports[`storybook Storyshots Components/HistoryTable/Icons Active Icons 1`] = `

5/10 failing tests

@@ -222,7 +222,7 @@ exports[`storybook Storyshots Components/HistoryTable/Icons Active Icons 1`] = `

5/10 failing tests

@@ -262,7 +262,7 @@ exports[`storybook Storyshots Components/HistoryTable/Icons Active Icons 1`] = `

5/10 failing tests

@@ -295,7 +295,7 @@ exports[`storybook Storyshots Components/HistoryTable/Icons Active Icons 1`] = `

diff --git a/src/components/HistoryTable/HistoryTableTestSearch/__snapshots__/HistoryTableTestSearch.stories.storyshot b/src/components/HistoryTable/HistoryTableTestSearch/__snapshots__/HistoryTableTestSearch.stories.storyshot index 7e8c157e73..9079a1757f 100644 --- a/src/components/HistoryTable/HistoryTableTestSearch/__snapshots__/HistoryTableTestSearch.stories.storyshot +++ b/src/components/HistoryTable/HistoryTableTestSearch/__snapshots__/HistoryTableTestSearch.stories.storyshot @@ -18,7 +18,7 @@ exports[`storybook Storyshots Components/HistoryTable/HistoryTableTestSearch Def class="leafygreen-ui-a533bt" >

evergreen smoke test @@ -76,7 +76,7 @@ exports[`storybook Storyshots components/ProjectSelect With Clickable Header 1`] class="css-1ry038z-Container e17dnzmx0" >

evergreen smoke test diff --git a/src/components/ProjectSelect/index.tsx b/src/components/ProjectSelect/index.tsx index 7722fe78e5..9a9dd6fdda 100644 --- a/src/components/ProjectSelect/index.tsx +++ b/src/components/ProjectSelect/index.tsx @@ -13,17 +13,19 @@ import { Unpacked } from "types/utils"; import { ProjectOptionGroup } from "./ProjectOptionGroup"; interface ProjectSelectProps { - selectedProjectIdentifier: string; - isProjectSettingsPage?: boolean; getRoute: (projectIdentifier: string) => string; + isProjectSettingsPage?: boolean; onSubmit?: (projectIdentifier: string) => void; + selectedProjectIdentifier: string; + showLabel?: boolean; } export const ProjectSelect: React.VFC = ({ - selectedProjectIdentifier, - isProjectSettingsPage = false, getRoute, + isProjectSettingsPage = false, onSubmit = () => {}, + selectedProjectIdentifier, + showLabel = true, }) => { const navigate = useNavigate(); @@ -90,7 +92,7 @@ export const ProjectSelect: React.VFC = ({ return (

Select an element @@ -76,7 +76,7 @@ exports[`storybook Storyshots components/SearchableDropdown Default 1`] = ` class="css-1ry038z-Container e17dnzmx0" >

Select an element diff --git a/src/components/SearchableDropdown/index.tsx b/src/components/SearchableDropdown/index.tsx index 8876880704..3eae60179e 100644 --- a/src/components/SearchableDropdown/index.tsx +++ b/src/components/SearchableDropdown/index.tsx @@ -1,6 +1,5 @@ import { ChangeEvent, - ReactNode, useState, PropsWithChildren, useRef, @@ -20,17 +19,17 @@ const { gray, blue } = palette; export interface SearchableDropdownProps { allowMultiSelect?: boolean; - buttonRenderer?: (option: T | T[]) => ReactNode; + buttonRenderer?: (option: T | T[]) => React.ReactNode; ["data-cy"]?: string; disabled?: boolean; - label: string | ReactNode; + label?: React.ReactNode; onChange: (value: T | T[]) => void; options?: T[] | string[]; optionRenderer?: ( option: T, onClick: (selectedV) => void, isChecked: (selectedV) => boolean - ) => ReactNode; + ) => React.ReactNode; searchFunc?: (options: T[], match: string) => T[]; searchPlaceholder?: string; value: T | T[]; @@ -144,7 +143,7 @@ const SearchableDropdown = ({ return ( - + {label && } (H3)` - margin: ${size.m} 0; +export const SettingsCardTitle = styled(H3)` + margin: ${size.m} 0 ${size.s} 0; `; export const formComponentSpacingCSS = "margin-bottom: 48px;"; diff --git a/src/components/SpruceForm/Container.tsx b/src/components/SpruceForm/Container.tsx index f0441e0276..59aeae0dcd 100644 --- a/src/components/SpruceForm/Container.tsx +++ b/src/components/SpruceForm/Container.tsx @@ -1,20 +1,23 @@ import { SettingsCard, SettingsCardTitle } from "components/SettingsCard"; interface ContainerProps { - title?: string; - id?: string; - "data-cy"?: string; children: React.ReactNode; + "data-cy"?: string; + description?: React.ReactNode; + id?: string; + title?: string; } export const SpruceFormContainer: React.VFC = ({ children, "data-cy": dataCy, + description, id, title, }) => (

{title && {title}} + {description} {children}
); diff --git a/src/components/SpruceForm/CustomFields.tsx b/src/components/SpruceForm/CustomFields.tsx index 5c031e498c..329731d272 100644 --- a/src/components/SpruceForm/CustomFields.tsx +++ b/src/components/SpruceForm/CustomFields.tsx @@ -1,5 +1,11 @@ import styled from "@emotion/styled"; -import { Description, H3, Subtitle } from "@leafygreen-ui/typography"; +import { + Description, + H3, + H3Props, + Subtitle, + SubtitleProps, +} from "@leafygreen-ui/typography"; import { Field, FieldProps } from "@rjsf/core"; import { size } from "constants/tokens"; @@ -12,20 +18,15 @@ export const TitleField: React.VFC = ({ }) => { const isSectionTitle = uiSchema?.["ui:sectionTitle"] ?? false; const Component = isSectionTitle ? StyledH3 : StyledSubtitle; - return ( - /* @ts-expect-error */ - {title} - ); + return {title}; }; -/* @ts-expect-error */ -const StyledH3 = styled(H3)` +const StyledH3 = styled(H3)` margin-top: ${size.m}; margin-bottom: 12px; `; -/* @ts-expect-error */ -const StyledSubtitle = styled(Subtitle)` +const StyledSubtitle = styled(Subtitle)` margin-top: ${size.s}; margin-bottom: 12px; `; diff --git a/src/components/SpruceForm/FieldTemplates/ArrayFieldTemplates/index.tsx b/src/components/SpruceForm/FieldTemplates/ArrayFieldTemplates/index.tsx index 080ea80228..05c6aa5c88 100644 --- a/src/components/SpruceForm/FieldTemplates/ArrayFieldTemplates/index.tsx +++ b/src/components/SpruceForm/FieldTemplates/ArrayFieldTemplates/index.tsx @@ -141,6 +141,8 @@ export const ArrayFieldTemplate: React.VFC = ({ const addButtonSize = uiSchema["ui:addButtonSize"] || "small"; const addButtonText = uiSchema["ui:addButtonText"] || "Add"; const secondaryButton = uiSchema["ui:secondaryButton"]; + const arrayDataCy = uiSchema["ui:data-cy"]; + // Override RJSF's default array behavior; add new elements to beginning of array unless otherwise specified. const addToEnd = uiSchema["ui:addToEnd"] ?? false; const handleAddClick = @@ -178,6 +180,7 @@ export const ArrayFieldTemplate: React.VFC = ({ {items.length === 0 ? ( {placeholder} diff --git a/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx b/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx index bcb83e32ce..a4bc2d9edd 100644 --- a/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx +++ b/src/components/SpruceForm/FieldTemplates/ObjectFieldTemplates/index.tsx @@ -53,19 +53,36 @@ export const ObjectFieldTemplate = ({ * `CardFieldTemplate` is a custom ObjectFieldTemplate that renders a card with a title and a list of properties. */ export const CardFieldTemplate: React.VFC = ({ + DescriptionField, idSchema, properties, + schema, title, - uiSchema: { "ui:title": uiTitle, "ui:data-cy": dataCy }, -}) => ( - - {properties.map((prop) => prop.content)} - -); + uiSchema: { + "ui:data-cy": dataCy, + "ui:description": uiDescription, + "ui:title": uiTitle, + }, +}) => { + const description = uiDescription || schema.description; + return ( + + ) + } + > + {properties.map((prop) => prop.content)} + + ); +}; /** * `AccordionFieldTemplate` is a custom ObjectFieldTemplate that renders an accordion with a title and a list of properties. @@ -116,7 +133,6 @@ const RowContainer = styled.div` gap: ${size.s}; `; -/* @ts-expect-error */ const AccordionTitle = styled(Subtitle)` font-size: ${fontSize.l}; margin: ${size.xs} 0; diff --git a/src/components/SpruceForm/SpruceForm.test.tsx b/src/components/SpruceForm/SpruceForm.test.tsx index 548ddf03d9..d343862144 100644 --- a/src/components/SpruceForm/SpruceForm.test.tsx +++ b/src/components/SpruceForm/SpruceForm.test.tsx @@ -306,6 +306,54 @@ describe("spruce form", () => { ).toHaveStyle("cursor: not-allowed"); }); }); + + describe("radio group", () => { + it("renders 3 inputs with the specified default selected", () => { + const { formData, schema, uiSchema } = radioGroup; + const onChange = jest.fn(); + render( + + ); + + expect(screen.getAllByRole("radio")).toHaveLength(3); + expect(screen.getByLabelText("New York")).toBeChecked(); + }); + + it("disables options in enumDisabled", () => { + const { formData, schema, uiSchema } = radioGroup; + const onChange = jest.fn(); + render( + + ); + + expect(screen.getByLabelText("Connecticut")).toBeDisabled(); + }); + + it("shows option descriptions", () => { + const { formData, schema, uiSchema } = radioGroup; + const onChange = jest.fn(); + render( + + ); + + expect(screen.getByText("The Garden State")).toBeVisible(); + }); + }); }); }); @@ -362,7 +410,6 @@ const basicForm = { }, users: { "ui:addButtonText": "New User", - "ui:data-cy": "new-user-input", items: { "ui:ariaLabelledBy": "root_access", "ui:data-cy": "new-user-input", @@ -449,3 +496,41 @@ const select = { }, }, }; + +const radioGroup = { + formData: {}, + schema: { + type: "object" as "object", + properties: { + states: { + type: "string" as "string", + title: "Tri-state Area", + default: "ny", + oneOf: [ + { + type: "string" as "string", + title: "New York", + enum: ["ny"], + }, + { + type: "string" as "string", + title: "New Jersey", + description: "The Garden State", + enum: ["nj"], + }, + { + type: "string" as "string", + title: "Connecticut", + enum: ["ct"], + }, + ], + }, + }, + }, + uiSchema: { + states: { + "ui:enumDisabled": ["ct"], + "ui:widget": "radio", + }, + }, +}; diff --git a/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx b/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx index 11fb1f3e19..021d809bc1 100644 --- a/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx +++ b/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef } from "react"; import styled from "@emotion/styled"; import Banner from "@leafygreen-ui/banner"; import Checkbox from "@leafygreen-ui/checkbox"; @@ -7,6 +8,7 @@ import { Radio, RadioGroup } from "@leafygreen-ui/radio-group"; import { SegmentedControl, SegmentedControlOption, + SegmentedControlProps, } from "@leafygreen-ui/segmented-control"; import { Option, Select } from "@leafygreen-ui/select"; import TextArea from "@leafygreen-ui/text-area"; @@ -232,11 +234,13 @@ export const LeafyGreenRadio: React.VFC = ({ > {enumOptions.map((o) => { const optionDisabled = enumDisabled?.includes(o.value) ?? false; + const { description } = o.schema ?? {}; return ( {o.label} @@ -327,21 +331,39 @@ const StyledRadioBox = styled(RadioBox)` `; export const LeafyGreenTextArea: React.VFC = ({ - label, disabled, - value, + label, onChange, options, rawErrors, readonly, + value, }) => { - const { "data-cy": dataCy, emptyValue = "", elementWrapperCSS } = options; + const { + "data-cy": dataCy, + elementWrapperCSS, + emptyValue = "", + focusOnMount, + } = options; const { errors, hasError } = processErrors(rawErrors); + const el = useRef(); + + useEffect(() => { + if (focusOnMount) { + const textarea = el.current; + if (textarea) { + textarea.focus(); + textarea.selectionStart = textarea.value.length; + textarea.selectionEnd = textarea.value.length; + } + } + }, [focusOnMount]); return (