diff --git a/assets/images/search/copilot-action.png b/assets/images/search/copilot-action.png new file mode 100644 index 000000000000..d8576d326c1a Binary files /dev/null and b/assets/images/search/copilot-action.png differ diff --git a/content/copilot/using-github-copilot/ai-models/choosing-the-right-ai-model-for-your-task.md b/content/copilot/using-github-copilot/ai-models/choosing-the-right-ai-model-for-your-task.md index 0f1520d324e0..07433afc4c16 100644 --- a/content/copilot/using-github-copilot/ai-models/choosing-the-right-ai-model-for-your-task.md +++ b/content/copilot/using-github-copilot/ai-models/choosing-the-right-ai-model-for-your-task.md @@ -237,7 +237,7 @@ The following table summarizes when an alternative model may be a better choice: OpenAI o3-mini is a fast, cost-effective reasoning model designed to deliver coding performance while maintaining lower latency and resource usage. o3-mini outperforms o1 on coding benchmarks with response times that are comparable to o1-mini. Copilot is configured to use OpenAI's "medium" reasoning effort. -For more information about o1, see [OpenAI's documentation](https://platform.openai.com/docs/models/o3-mini). +For more information about o3-mini, see [OpenAI's documentation](https://platform.openai.com/docs/models/o3-mini). ### Use cases diff --git a/content/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/index.md b/content/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/index.md index e79e561d3b9d..c137791b55b2 100644 --- a/content/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/index.md +++ b/content/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/index.md @@ -19,6 +19,6 @@ children: - /about-email-notifications-for-pushes-to-your-repository - /configuring-autolinks-to-reference-external-resources - /configuring-tag-protection-rules + - /managing-auto-closing-issues shortTitle: Manage repository settings --- - diff --git a/content/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-auto-closing-issues.md b/content/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-auto-closing-issues.md new file mode 100644 index 000000000000..5def2d6ae99d --- /dev/null +++ b/content/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-auto-closing-issues.md @@ -0,0 +1,26 @@ +--- +title: Managing the automatic closing of issues in your repository +intro: You can select whether merged linked pull requests will auto-close your issues. +versions: + fpt: '*' + ghes: '*' + ghec: '*' +permissions: Repository administrators and maintainers can configure the automating closing of issues in the repository, once related pull requests are merged. +topics: + - Repositories + - Issues + - Pull requests +shortTitle: Manage auto-closing issues +allowTitleToDifferFromFilename: true +--- + +## About auto-closing issues + +By default, merging a linked pull request automatically closes the associated issue. You can override the default behavior by disabling auto-closing. + +## Enabling or disabling auto-closing of issues + +{% data reusables.repositories.navigate-to-repo %} +{% data reusables.repositories.sidebar-settings %} +1. Under **General**, scroll down to the **Issues** section. +1. Select or deselect **Auto-close issues with merged linked pull requests** to enable or disable auto-closing. diff --git a/data/ui.yml b/data/ui.yml index 68a42aca1ff3..89554d3edea4 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -64,6 +64,9 @@ search: general_title: There was an error loading search results. ai_title: There was an error loading Copilot. description: You can still use this field to search our docs. + cta: + heading: New! Copilot for Docs + description: Ask your question in the search bar and get help in seconds. old_search: description: Enter a search term to find it in the GitHub Docs. placeholder: Search GitHub Docs diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml index 68a42aca1ff3..89554d3edea4 100644 --- a/src/fixtures/fixtures/data/ui.yml +++ b/src/fixtures/fixtures/data/ui.yml @@ -64,6 +64,9 @@ search: general_title: There was an error loading search results. ai_title: There was an error loading Copilot. description: You can still use this field to search our docs. + cta: + heading: New! Copilot for Docs + description: Ask your question in the search bar and get help in seconds. old_search: description: Enter a search term to find it in the GitHub Docs. placeholder: Search GitHub Docs diff --git a/src/frame/components/context/CTAContext.tsx b/src/frame/components/context/CTAContext.tsx new file mode 100644 index 000000000000..474e8f0e870f --- /dev/null +++ b/src/frame/components/context/CTAContext.tsx @@ -0,0 +1,78 @@ +// Context to keep track of a call to action (e.g. popover introducing a new feature) +// The state of the CTA will be stored in local storage, so it will persist across page reloads +// If `dismiss` is called, the CTA will not be shown again +import { + createContext, + useCallback, + useContext, + useEffect, + useState, + PropsWithChildren, +} from 'react' + +type CTAPopoverState = { + isOpen: boolean + initializeCTA: () => void // Call to "open" the CTA if it's not already been dismissed by the user + dismiss: () => void // Call to "close" the CTA and store the dismissal in local storage +} + +type StoredValue = { dismissed: true } + +const CTAPopoverContext = createContext(undefined) + +const STORAGE_KEY = 'ctaPopoverDismissed' + +const isDismissed = (): boolean => { + if (typeof window === 'undefined') return false // SSR guard + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return false + const parsed = JSON.parse(raw) as StoredValue + return parsed?.dismissed + } catch { + return false // corruption / quota / disabled storage + } +} + +export function CTAPopoverProvider({ children }: PropsWithChildren) { + // We start closed because we might only want to "turn on" the CTA if an experiment is active + const [isOpen, setIsOpen] = useState(false) + + const persistDismissal = useCallback(() => { + setIsOpen(false) + try { + const obj: StoredValue = { dismissed: true } + localStorage.setItem(STORAGE_KEY, JSON.stringify(obj)) + } catch { + /* ignore */ + } + }, []) + + const dismiss = useCallback(() => persistDismissal(), [persistDismissal]) + const initializeCTA = useCallback(() => { + const dismissed = isDismissed() + if (dismissed) { + setIsOpen(false) + } else { + setIsOpen(true) + } + }, [isDismissed]) + + // Wrap in a useEffect to avoid a hydration mismatch (SSR guard) + useEffect(() => { + const stored = isDismissed() + setIsOpen(!stored) + }, []) + + return ( + + {children} + + ) +} + +export const useCTAPopoverContext = () => { + const ctx = useContext(CTAPopoverContext) + if (!ctx) throw new Error('useCTAPopoverContext must be used inside ') + return ctx +} diff --git a/src/frame/components/page-header/Header.tsx b/src/frame/components/page-header/Header.tsx index fb5bce71a4b0..26e3cc67d2f9 100644 --- a/src/frame/components/page-header/Header.tsx +++ b/src/frame/components/page-header/Header.tsx @@ -23,6 +23,7 @@ import { useShouldShowExperiment } from '@/events/components/experiments/useShou import { useQueryParam } from '@/frame/components/hooks/useQueryParam' import { useMultiQueryParams } from '@/search/components/hooks/useMultiQueryParams' import { SearchOverlayContainer } from '@/search/components/input/SearchOverlayContainer' +import { useCTAPopoverContext } from '@/frame/components/context/CTAContext' import styles from './Header.module.scss' @@ -50,6 +51,7 @@ export const Header = () => { const { width } = useInnerWindowWidth() const returnFocusRef = useRef(null) const searchButtonRef = useRef(null) + const { initializeCTA } = useCTAPopoverContext() const showNewSearch = useShouldShowExperiment(EXPERIMENTS.ai_search_experiment) let SearchButton: JSX.Element | null = ( @@ -62,6 +64,8 @@ export const Header = () => { ) if (!showNewSearch) { SearchButton = null + } else { + initializeCTA() } useEffect(() => { diff --git a/src/frame/pages/app.tsx b/src/frame/pages/app.tsx index 26434c57c49b..aa60382cb8be 100644 --- a/src/frame/pages/app.tsx +++ b/src/frame/pages/app.tsx @@ -17,6 +17,7 @@ import { } from 'src/languages/components/LanguagesContext' import { useTheme } from 'src/color-schemes/components/useTheme' import { SharedUIContextProvider } from 'src/frame/components/context/SharedUIContext' +import { CTAPopoverProvider } from 'src/frame/components/context/CTAContext' type MyAppProps = AppProps & { isDotComAuthenticated: boolean @@ -140,7 +141,9 @@ const MyApp = ({ Component, pageProps, languagesContext, stagingName }: MyAppPro > - + + + diff --git a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json index a2f03314d2b8..e43131bcfd6a 100644 --- a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json +++ b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json @@ -8222,6 +8222,15 @@ "requestPath": "/users/{username}/settings/billing/shared-storage", "additional-permissions": false, "access": "read" + }, + { + "category": "billing", + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage", + "additional-permissions": false, + "access": "read" } ] }, diff --git a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json index 1428cda1aa64..6e05c5a3c339 100644 --- a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json +++ b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat.json @@ -1085,6 +1085,12 @@ "subcategory": "billing", "verb": "get", "requestPath": "/users/{username}/settings/billing/shared-storage" + }, + { + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage" } ], "branches": [ diff --git a/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json b/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json index 33d642d205c2..d8e48bc07706 100644 --- a/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json +++ b/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json @@ -9981,6 +9981,17 @@ "user-to-server": true, "server-to-server": false, "additional-permissions": false + }, + { + "category": "billing", + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage", + "access": "read", + "user-to-server": true, + "server-to-server": false, + "additional-permissions": false } ] }, diff --git a/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json b/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json index 1428cda1aa64..6e05c5a3c339 100644 --- a/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json +++ b/src/github-apps/data/fpt-2022-11-28/user-to-server-rest.json @@ -1085,6 +1085,12 @@ "subcategory": "billing", "verb": "get", "requestPath": "/users/{username}/settings/billing/shared-storage" + }, + { + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage" } ], "branches": [ diff --git a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json index 589a0550cb36..6e9351af72bb 100644 --- a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json +++ b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json @@ -8921,6 +8921,15 @@ "requestPath": "/users/{username}/settings/billing/shared-storage", "additional-permissions": false, "access": "read" + }, + { + "category": "billing", + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage", + "additional-permissions": false, + "access": "read" } ] }, diff --git a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json index e0a5b7027530..bb727fa30010 100644 --- a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json +++ b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json @@ -1123,6 +1123,12 @@ "subcategory": "billing", "verb": "get", "requestPath": "/users/{username}/settings/billing/shared-storage" + }, + { + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage" } ], "branches": [ diff --git a/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json b/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json index c4cb1a843054..be34c0a45889 100644 --- a/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json +++ b/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json @@ -10830,6 +10830,17 @@ "user-to-server": true, "server-to-server": false, "additional-permissions": false + }, + { + "category": "billing", + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage", + "access": "read", + "user-to-server": true, + "server-to-server": false, + "additional-permissions": false } ] }, diff --git a/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json b/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json index e0a5b7027530..bb727fa30010 100644 --- a/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json +++ b/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json @@ -1123,6 +1123,12 @@ "subcategory": "billing", "verb": "get", "requestPath": "/users/{username}/settings/billing/shared-storage" + }, + { + "slug": "get-billing-usage-report-for-a-user", + "subcategory": "enhanced-billing", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage" } ], "branches": [ diff --git a/src/github-apps/lib/config.json b/src/github-apps/lib/config.json index f0dc1f1bb311..dfb94ff25170 100644 --- a/src/github-apps/lib/config.json +++ b/src/github-apps/lib/config.json @@ -60,5 +60,5 @@ "2022-11-28" ] }, - "sha": "1c22f3361af92533fad97262cf4b7c18574f22a6" + "sha": "467f6a98a97a73630a1f1e75ddee01cb59f33dd5" } \ No newline at end of file diff --git a/src/rest/data/fpt-2022-11-28/schema.json b/src/rest/data/fpt-2022-11-28/schema.json index a850ae9e8a06..fa30b59619d3 100644 --- a/src/rest/data/fpt-2022-11-28/schema.json +++ b/src/rest/data/fpt-2022-11-28/schema.json @@ -109554,6 +109554,193 @@ "description": "

Service unavailable

" } ] + }, + { + "serverUrl": "https://api.github.com", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage", + "title": "Get billing usage report for a user", + "category": "billing", + "subcategory": "enhanced-billing", + "parameters": [ + { + "name": "username", + "description": "

The handle for the GitHub user account.

", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "year", + "description": "

If specified, only return results for a single year. The value of year is an integer with four digits representing a year. For example, 2025. Default value is the current year.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "month", + "description": "

If specified, only return results for a single month. The value of month is an integer between 1 and 12. If no year is specified the default year is used.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "day", + "description": "

If specified, only return results for a single day. The value of day is an integer between 1 and 31. If no year or month is specified, the default year and month are used.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "hour", + "description": "

If specified, only return results for a single hour. The value of hour is an integer between 0 and 23. If no year, month, or day is specified, the default year, month, and day are used.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "bodyParameters": [], + "progAccess": { + "userToServerRest": true, + "serverToServer": false, + "fineGrainedPat": true, + "permissions": [ + { + "\"Plan\" user permissions": "read" + } + ] + }, + "codeExamples": [ + { + "key": "default", + "request": { + "description": "Example", + "acceptHeader": "application/vnd.github.v3+json", + "parameters": { + "username": "USERNAME" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Response when getting a billing usage report

", + "example": { + "usageItems": [ + { + "date": "2023-08-01", + "product": "Actions", + "sku": "Actions Linux", + "quantity": 100, + "unitType": "minutes", + "pricePerUnit": 0.008, + "grossAmount": 0.8, + "discountAmount": 0, + "netAmount": 0.8, + "repositoryName": "user/example" + } + ] + }, + "schema": { + "type": "object", + "properties": { + "usageItems": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "description": "Date of the usage line item." + }, + "product": { + "type": "string", + "description": "Product name." + }, + "sku": { + "type": "string", + "description": "SKU name." + }, + "quantity": { + "type": "integer", + "description": "Quantity of the usage line item." + }, + "unitType": { + "type": "string", + "description": "Unit type of the usage line item." + }, + "pricePerUnit": { + "type": "number", + "description": "Price per unit of the usage line item." + }, + "grossAmount": { + "type": "number", + "description": "Gross amount of the usage line item." + }, + "discountAmount": { + "type": "number", + "description": "Discount amount of the usage line item." + }, + "netAmount": { + "type": "number", + "description": "Net amount of the usage line item." + }, + "repositoryName": { + "type": "string", + "description": "Name of the repository." + } + }, + "required": [ + "date", + "product", + "sku", + "quantity", + "unitType", + "pricePerUnit", + "grossAmount", + "discountAmount", + "netAmount" + ] + } + } + } + } + } + } + ], + "previews": [], + "descriptionHTML": "

Gets a report of the total usage for a user.

\n

Note: This endpoint is only available to users with access to the enhanced billing platform.

", + "statusCodes": [ + { + "httpStatusCode": "200", + "description": "

Response when getting a billing usage report

" + }, + { + "httpStatusCode": "400", + "description": "

Bad Request

" + }, + { + "httpStatusCode": "403", + "description": "

Forbidden

" + }, + { + "httpStatusCode": "500", + "description": "

Internal Error

" + }, + { + "httpStatusCode": "503", + "description": "

Service unavailable

" + } + ] } ] }, @@ -163542,6 +163729,10 @@ "httpStatusCode": "404", "description": "

Resource not found

" }, + { + "httpStatusCode": "422", + "description": "

Response if analysis could not be processed

" + }, { "httpStatusCode": "503", "description": "

Service unavailable

" @@ -566367,7 +566558,7 @@ "type": "string or null", "name": "resolution_comment", "in": "body", - "description": "

An optional comment when closing an alert. Cannot be updated or deleted. Must be null when changing state to open.

" + "description": "

An optional comment when closing or reopening an alert. Cannot be updated or deleted.

" } ], "progAccess": { diff --git a/src/rest/data/ghec-2022-11-28/schema.json b/src/rest/data/ghec-2022-11-28/schema.json index 41feacc14258..4e6c37b4fd14 100644 --- a/src/rest/data/ghec-2022-11-28/schema.json +++ b/src/rest/data/ghec-2022-11-28/schema.json @@ -118970,6 +118970,193 @@ "description": "

Service unavailable

" } ] + }, + { + "serverUrl": "https://api.github.com", + "verb": "get", + "requestPath": "/users/{username}/settings/billing/usage", + "title": "Get billing usage report for a user", + "category": "billing", + "subcategory": "enhanced-billing", + "parameters": [ + { + "name": "username", + "description": "

The handle for the GitHub user account.

", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "year", + "description": "

If specified, only return results for a single year. The value of year is an integer with four digits representing a year. For example, 2025. Default value is the current year.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "month", + "description": "

If specified, only return results for a single month. The value of month is an integer between 1 and 12. If no year is specified the default year is used.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "day", + "description": "

If specified, only return results for a single day. The value of day is an integer between 1 and 31. If no year or month is specified, the default year and month are used.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "hour", + "description": "

If specified, only return results for a single hour. The value of hour is an integer between 0 and 23. If no year, month, or day is specified, the default year, month, and day are used.

", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "bodyParameters": [], + "progAccess": { + "userToServerRest": true, + "serverToServer": false, + "fineGrainedPat": true, + "permissions": [ + { + "\"Plan\" user permissions": "read" + } + ] + }, + "codeExamples": [ + { + "key": "default", + "request": { + "description": "Example", + "acceptHeader": "application/vnd.github.v3+json", + "parameters": { + "username": "USERNAME" + } + }, + "response": { + "statusCode": "200", + "contentType": "application/json", + "description": "

Response when getting a billing usage report

", + "example": { + "usageItems": [ + { + "date": "2023-08-01", + "product": "Actions", + "sku": "Actions Linux", + "quantity": 100, + "unitType": "minutes", + "pricePerUnit": 0.008, + "grossAmount": 0.8, + "discountAmount": 0, + "netAmount": 0.8, + "repositoryName": "user/example" + } + ] + }, + "schema": { + "type": "object", + "properties": { + "usageItems": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "description": "Date of the usage line item." + }, + "product": { + "type": "string", + "description": "Product name." + }, + "sku": { + "type": "string", + "description": "SKU name." + }, + "quantity": { + "type": "integer", + "description": "Quantity of the usage line item." + }, + "unitType": { + "type": "string", + "description": "Unit type of the usage line item." + }, + "pricePerUnit": { + "type": "number", + "description": "Price per unit of the usage line item." + }, + "grossAmount": { + "type": "number", + "description": "Gross amount of the usage line item." + }, + "discountAmount": { + "type": "number", + "description": "Discount amount of the usage line item." + }, + "netAmount": { + "type": "number", + "description": "Net amount of the usage line item." + }, + "repositoryName": { + "type": "string", + "description": "Name of the repository." + } + }, + "required": [ + "date", + "product", + "sku", + "quantity", + "unitType", + "pricePerUnit", + "grossAmount", + "discountAmount", + "netAmount" + ] + } + } + } + } + } + } + ], + "previews": [], + "descriptionHTML": "

Gets a report of the total usage for a user.

\n

Note: This endpoint is only available to users with access to the enhanced billing platform.

", + "statusCodes": [ + { + "httpStatusCode": "200", + "description": "

Response when getting a billing usage report

" + }, + { + "httpStatusCode": "400", + "description": "

Bad Request

" + }, + { + "httpStatusCode": "403", + "description": "

Forbidden

" + }, + { + "httpStatusCode": "500", + "description": "

Internal Error

" + }, + { + "httpStatusCode": "503", + "description": "

Service unavailable

" + } + ] } ] }, @@ -174626,6 +174813,10 @@ "httpStatusCode": "404", "description": "

Resource not found

" }, + { + "httpStatusCode": "422", + "description": "

Response if analysis could not be processed

" + }, { "httpStatusCode": "503", "description": "

Service unavailable

" @@ -607500,7 +607691,7 @@ "type": "string or null", "name": "resolution_comment", "in": "body", - "description": "

An optional comment when closing an alert. Cannot be updated or deleted. Must be null when changing state to open.

" + "description": "

An optional comment when closing or reopening an alert. Cannot be updated or deleted.

" } ], "progAccess": { diff --git a/src/rest/data/ghes-3.16-2022-11-28/schema.json b/src/rest/data/ghes-3.16-2022-11-28/schema.json index 01c1c260154e..7af4ff704410 100644 --- a/src/rest/data/ghes-3.16-2022-11-28/schema.json +++ b/src/rest/data/ghes-3.16-2022-11-28/schema.json @@ -514751,7 +514751,7 @@ "type": "string or null", "name": "resolution_comment", "in": "body", - "description": "

An optional comment when closing an alert. Cannot be updated or deleted. Must be null when changing state to open.

" + "description": "

An optional comment when closing or reopening an alert. Cannot be updated or deleted.

" } ], "progAccess": { diff --git a/src/rest/lib/config.json b/src/rest/lib/config.json index 146985f66f09..4b57538a19d8 100644 --- a/src/rest/lib/config.json +++ b/src/rest/lib/config.json @@ -47,5 +47,5 @@ ] } }, - "sha": "1c22f3361af92533fad97262cf4b7c18574f22a6" + "sha": "467f6a98a97a73630a1f1e75ddee01cb59f33dd5" } \ No newline at end of file diff --git a/src/search/components/hooks/useBreakpoint.ts b/src/search/components/hooks/useBreakpoint.ts index 2993646c411c..b3b8651462e8 100644 --- a/src/search/components/hooks/useBreakpoint.ts +++ b/src/search/components/hooks/useBreakpoint.ts @@ -2,8 +2,20 @@ import { useTheme } from '@primer/react' import { useMediaQuery } from './useMediaQuery' -type Size = 'small' | 'medium' | 'large' | 'xlarge' -export function useBreakpoint(size: Size) { +type Size = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' +export function useMinWidthBreakpoint(size: Size) { const { theme } = useTheme() - return useMediaQuery(`(min-width: ${theme?.sizes[size]})`) + // For some reason, xsmall isn't in theme for Primer: https://github.com/primer/react/blob/308fe82909f3d922be0a6582f83e96798678ec78/packages/react/src/utils/layout.ts#L6 + let sizePx = theme?.sizes[size] + if (size === 'xsmall') { + sizePx = '320px' + } + return useMediaQuery(`(min-width: ${sizePx})`) +} + +export function useMaxWidthBreakpoint(sizePx: string) { + if (!sizePx.endsWith('px')) { + sizePx = `${sizePx}px` + } + return useMediaQuery(`(max-width: ${sizePx})`) } diff --git a/src/search/components/input/AISearchCTAPopup.tsx b/src/search/components/input/AISearchCTAPopup.tsx new file mode 100644 index 000000000000..de05812fce93 --- /dev/null +++ b/src/search/components/input/AISearchCTAPopup.tsx @@ -0,0 +1,107 @@ +import { useEffect, useRef } from 'react' +import { Text, Button, Heading, Popover, useOnEscapePress } from '@primer/react' +import { focusTrap } from '@primer/behaviors' + +import { useTranslation } from '@/languages/components/useTranslation' +import { useMaxWidthBreakpoint, useMinWidthBreakpoint } from '../hooks/useBreakpoint' + +let previouslyFocused: HTMLElement | null = null + +export function AISearchCTAPopup({ isOpen, dismiss }: { isOpen: boolean; dismiss: () => void }) { + const { t } = useTranslation('search') + const isLargeOrUp = useMinWidthBreakpoint('large') + const isTooSmallForCTA = useMaxWidthBreakpoint('293px') + let overlayRef = useRef(null) + let dismissButtonRef = useRef(null) + + // For a11y, focus trap the CTA and allow it to be closed with Escape + useEffect(() => { + if (isTooSmallForCTA) { + return + } + if (isOpen && overlayRef.current && dismissButtonRef.current) { + focusTrap(overlayRef.current, dismissButtonRef.current) + previouslyFocused = document.activeElement as HTMLElement | null + } + }, [isOpen, isTooSmallForCTA]) + + const onDismiss = () => { + if (isTooSmallForCTA) { + return + } + if (previouslyFocused) { + previouslyFocused.focus() + } + dismiss() + } + + useOnEscapePress(onDismiss) + + if (isTooSmallForCTA) { + return null + } + + return ( + + + The Copilot Icon in front of an explosion of color. + + {t('search.cta.heading')} + + + {t('search.cta.description')} + + + + + ) +} diff --git a/src/search/components/input/AskAIResults.tsx b/src/search/components/input/AskAIResults.tsx index 163c205c6d4a..9ebc584b1134 100644 --- a/src/search/components/input/AskAIResults.tsx +++ b/src/search/components/input/AskAIResults.tsx @@ -15,6 +15,8 @@ import { sendEvent, uuidv4 } from '@/events/components/events' import { EventType } from '@/events/types' import { generateAISearchLinksJson } from '../helpers/ai-search-links-json' import { ASK_AI_EVENT_GROUP } from '@/events/components/event-groups' +import { useCTAPopoverContext } from '@/frame/components/context/CTAContext' + import type { AIReference } from '../types' type AIQueryResultsProps = { @@ -74,6 +76,7 @@ export function AskAIResults({ aiCouldNotAnswer: boolean connectedEventId?: string }>('ai-query-cache', 1000, 7) + const { isOpen: isCTAOpen, dismiss: dismissCTA } = useCTAPopoverContext() const [isCopied, setCopied] = useClipboard(message, { successDuration: 1400 }) const [feedbackSelected, setFeedbackSelected] = useState(null) @@ -128,6 +131,11 @@ export function AskAIResults({ setResponseLoading(true) disclaimerRef.current?.focus() + // Upon performing an AI Search, dismiss the CTA if it is open + if (isCTAOpen) { + dismissCTA() + } + const cachedData = getItem(query, version, router.locale || 'en') if (cachedData) { setMessage(cachedData.message) diff --git a/src/search/components/input/OldSearchInput.tsx b/src/search/components/input/OldSearchInput.tsx index 578ef9211efe..9c9d32cc6585 100644 --- a/src/search/components/input/OldSearchInput.tsx +++ b/src/search/components/input/OldSearchInput.tsx @@ -6,7 +6,7 @@ import { SearchIcon } from '@primer/octicons-react' import { useTranslation } from 'src/languages/components/useTranslation' import { DEFAULT_VERSION, useVersion } from 'src/versions/components/useVersion' import { useQuery } from 'src/search/components/hooks/useQuery' -import { useBreakpoint } from 'src/search/components/hooks/useBreakpoint' +import { useMinWidthBreakpoint } from 'src/search/components/hooks/useBreakpoint' import { sendEvent } from 'src/events/components/events' import { EventType } from 'src/events/types' import { GENERAL_SEARCH_CONTEXT } from '../helpers/execute-search-actions' @@ -19,7 +19,7 @@ export function OldSearchInput({ isSearchOpen }: Props) { const [localQuery, setLocalQuery] = useState(query) const { t } = useTranslation('old_search') const { currentVersion } = useVersion() - const atMediumViewport = useBreakpoint('medium') + const atMediumViewport = useMinWidthBreakpoint('medium') function redirectSearch() { let asPath = `/${router.locale}` diff --git a/src/search/components/input/SearchBarButton.tsx b/src/search/components/input/SearchBarButton.tsx index b689db25932c..cfe07a178da5 100644 --- a/src/search/components/input/SearchBarButton.tsx +++ b/src/search/components/input/SearchBarButton.tsx @@ -3,9 +3,11 @@ import { IconButton } from '@primer/react' import { CopilotIcon, SearchIcon } from '@primer/octicons-react' import { useTranslation } from 'src/languages/components/useTranslation' +import { QueryParams } from '@/search/components/hooks/useMultiQueryParams' +import { useCTAPopoverContext } from '@/frame/components/context/CTAContext' import styles from './SearchBarButton.module.scss' -import { QueryParams } from '../hooks/useMultiQueryParams' +import { AISearchCTAPopup } from './AISearchCTAPopup' type Props = { isSearchOpen: boolean @@ -16,6 +18,7 @@ type Props = { export function SearchBarButton({ isSearchOpen, setIsSearchOpen, params, searchButtonRef }: Props) { const { t } = useTranslation('search') + const { isOpen, dismiss } = useCTAPopoverContext() const urlSearchInputQuery = params['search-overlay-input'] @@ -53,6 +56,7 @@ export function SearchBarButton({ isSearchOpen, setIsSearchOpen, params, searchB {/* We don't want to show the input when overlay is open */} {!isSearchOpen ? ( <> + {/* On mobile only the IconButton is shown */}