Compare commits

...

7 Commits

Author SHA1 Message Date
Tom Moor 099d0c8018 v0.85.1 2025-07-10 21:14:26 -04:00
Tom Moor 6334de29cc fix: EditorUpdateError code outside valid range (#9600) 2025-07-10 21:12:32 -04:00
Tom Moor 8e7c0284a4 fix: Restore preloading of notifications (#9583) 2025-07-10 21:11:42 -04:00
Tom Moor 590a02b124 fix: Buttons on not found page don't work (#9592) 2025-07-10 21:09:41 -04:00
Hemachandar 6fcd9f6ac2 fix: Navigate routes and links in kbar (#9552) 2025-07-10 21:09:23 -04:00
Tom Moor 79b2886852 fix: Cannot ctrl-click to open in a new tab some menu items (#9542)
* wip

* done
2025-07-10 21:08:55 -04:00
codegen-sh[bot] 57ac5f54bf Fix OIDC well-known discovery for subdirectories (#9540)
* Fix OIDC well-known discovery for subdirectories

- Fix URL construction in fetchOIDCConfiguration to properly handle issuer URLs with subdirectories
- Replace incorrect use of new URL() constructor that was treating well-known path as absolute
- Add proper path concatenation that preserves subdirectories in issuer URLs
- Add comprehensive test cases for subdirectory scenarios
- Fixes issue where https://auth.example.com/application/o/outline/ would incorrectly resolve to https://auth.example.com/.well-known/openid-configuration instead of https://auth.example.com/application/o/outline/.well-known/openid-configuration

Fixes #9535

* Refactor to use wellKnownPath variable instead of hardcoded path

- Use wellKnownPath.substring(1) to remove leading slash when appending to pathname
- Eliminates duplication of the .well-known/openid-configuration path
- Improves maintainability by using the existing variable consistently

* Simplify logic by checking pathname does not end with slash

- If pathname doesn't end with slash, append full wellKnownPath (with leading slash)
- If pathname ends with slash, append wellKnownPath without leading slash
- Eliminates need for substring() by using the slash logic more elegantly

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-07-10 21:08:38 -04:00
19 changed files with 197 additions and 48 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.85.0
Licensed Work: Outline 0.85.1
The Licensed Work is (c) 2025 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2029-07-03
Change Date: 2029-07-11
Change License: Apache License, Version 2.0
+1 -1
View File
@@ -47,7 +47,7 @@ export const openCollection = createAction({
name: collection.name,
icon: <ColorCollectionIcon collection={collection} />,
section: CollectionSection,
perform: () => history.push(collection.path),
to: collection.path,
}));
},
});
+2 -2
View File
@@ -98,7 +98,7 @@ export const openDocument = createAction({
<DocumentIcon />
),
section: DocumentSection,
perform: () => history.push(item.url),
to: item.url,
}));
},
});
@@ -840,7 +840,7 @@ export const searchDocumentsForQuery = (query: string) =>
analyticsName: "Search documents",
section: DocumentSection,
icon: <SearchIcon />,
perform: () => history.push(searchPath({ query })),
to: searchPath({ query }),
visible: ({ location }) => location.pathname !== searchPath(),
});
+35 -20
View File
@@ -21,7 +21,6 @@ import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { createAction } from "~/actions";
import { NavigationSection, RecentSearchesSection } from "~/actions/sections";
import Desktop from "~/utils/Desktop";
import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted";
import {
homePath,
@@ -38,7 +37,7 @@ export const navigateToHome = createAction({
section: NavigationSection,
shortcut: ["d"],
icon: <HomeIcon />,
perform: () => history.push(homePath()),
to: homePath(),
visible: ({ location }) => location.pathname !== homePath(),
});
@@ -48,7 +47,7 @@ export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
name: searchQuery.query,
analyticsName: "Navigate to recent search query",
icon: <SearchIcon />,
perform: () => history.push(searchPath({ query: searchQuery.query })),
to: searchPath({ query: searchQuery.query }),
});
export const navigateToDrafts = createAction({
@@ -56,7 +55,7 @@ export const navigateToDrafts = createAction({
analyticsName: "Navigate to drafts",
section: NavigationSection,
icon: <DraftsIcon />,
perform: () => history.push(draftsPath()),
to: draftsPath(),
visible: ({ location }) => location.pathname !== draftsPath(),
});
@@ -65,7 +64,7 @@ export const navigateToSearch = createAction({
analyticsName: "Navigate to search",
section: NavigationSection,
icon: <SearchIcon />,
perform: () => history.push(searchPath()),
to: searchPath(),
visible: ({ location }) => location.pathname !== searchPath(),
});
@@ -75,7 +74,7 @@ export const navigateToArchive = createAction({
section: NavigationSection,
shortcut: ["g", "a"],
icon: <ArchiveIcon />,
perform: () => history.push(archivePath()),
to: archivePath(),
visible: ({ location }) => location.pathname !== archivePath(),
});
@@ -84,7 +83,7 @@ export const navigateToTrash = createAction({
analyticsName: "Navigate to trash",
section: NavigationSection,
icon: <TrashIcon />,
perform: () => history.push(trashPath()),
to: trashPath(),
visible: ({ location }) => location.pathname !== trashPath(),
});
@@ -95,7 +94,7 @@ export const navigateToSettings = createAction({
shortcut: ["g", "s"],
icon: <SettingsIcon />,
visible: () => stores.policies.abilities(stores.auth.team?.id || "").update,
perform: () => history.push(settingsPath()),
to: settingsPath(),
});
export const navigateToWorkspaceSettings = createAction({
@@ -104,7 +103,7 @@ export const navigateToWorkspaceSettings = createAction({
section: NavigationSection,
icon: <SettingsIcon />,
visible: () => stores.policies.abilities(stores.auth.team?.id || "").update,
perform: () => history.push(settingsPath("details")),
to: settingsPath("details"),
});
export const navigateToProfileSettings = createAction({
@@ -113,7 +112,7 @@ export const navigateToProfileSettings = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <ProfileIcon />,
perform: () => history.push(settingsPath()),
to: settingsPath(),
});
export const navigateToTemplateSettings = createAction({
@@ -122,7 +121,7 @@ export const navigateToTemplateSettings = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <ShapesIcon />,
perform: () => history.push(settingsPath("templates")),
to: settingsPath("templates"),
});
export const navigateToNotificationSettings = createAction({
@@ -131,7 +130,7 @@ export const navigateToNotificationSettings = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <EmailIcon />,
perform: () => history.push(settingsPath("notifications")),
to: settingsPath("notifications"),
});
export const navigateToAccountPreferences = createAction({
@@ -140,7 +139,7 @@ export const navigateToAccountPreferences = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <SettingsIcon />,
perform: () => history.push(settingsPath("preferences")),
to: settingsPath("preferences"),
});
export const openDocumentation = createAction({
@@ -149,7 +148,10 @@ export const openDocumentation = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <OpenIcon />,
perform: () => window.open(UrlHelper.guide),
to: {
url: UrlHelper.guide,
target: "_blank",
},
});
export const openAPIDocumentation = createAction({
@@ -158,7 +160,10 @@ export const openAPIDocumentation = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <OpenIcon />,
perform: () => window.open(UrlHelper.developers),
to: {
url: UrlHelper.developers,
target: "_blank",
},
});
export const toggleSidebar = createAction({
@@ -175,14 +180,20 @@ export const openFeedbackUrl = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <EmailIcon />,
perform: () => window.open(UrlHelper.contact),
to: {
url: UrlHelper.contact,
target: "_blank",
},
});
export const openBugReportUrl = createAction({
name: ({ t }) => t("Report a bug"),
analyticsName: "Open bug report",
section: NavigationSection,
perform: () => window.open(UrlHelper.github),
to: {
url: UrlHelper.github,
target: "_blank",
},
});
export const openChangelog = createAction({
@@ -191,7 +202,10 @@ export const openChangelog = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <OpenIcon />,
perform: () => window.open(UrlHelper.changelog),
to: {
url: UrlHelper.changelog,
target: "_blank",
},
});
export const openKeyboardShortcuts = createAction({
@@ -219,8 +233,9 @@ export const downloadApp = createAction({
iconInContextMenu: false,
icon: <BrowserIcon />,
visible: () => !Desktop.isElectron() && isMac() && isCloudHosted,
perform: () => {
window.open("https://desktop.getoutline.com");
to: {
url: "https://desktop.getoutline.com",
target: "_blank",
},
});
+4 -1
View File
@@ -32,7 +32,10 @@ export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
);
},
visible: ({ currentTeamId }: ActionContext) => currentTeamId !== session.id,
perform: () => (window.location.href = session.url),
to: {
url: session.url,
target: "_self",
},
})) ?? [];
export const switchTeam = createAction({
+35 -6
View File
@@ -6,10 +6,13 @@ import {
Action,
ActionContext,
CommandBarAction,
MenuExternalLink,
MenuInternalLink,
MenuItemButton,
MenuItemWithChildren,
} from "~/types";
import Analytics from "~/utils/Analytics";
import history from "~/utils/history";
function resolve<T>(value: any, context: ActionContext): T {
return typeof value === "function" ? value(context) : value;
@@ -31,7 +34,6 @@ export function createAction(definition: Optional<Action, "id">): Action {
: "contextmenu",
});
}
return definition.perform?.(context);
}
: undefined,
@@ -42,7 +44,7 @@ export function createAction(definition: Optional<Action, "id">): Action {
export function actionToMenuItem(
action: Action,
context: ActionContext
): MenuItemButton | MenuItemWithChildren {
): MenuItemButton | MenuExternalLink | MenuInternalLink | MenuItemWithChildren {
const resolvedIcon = resolve<React.ReactElement<any>>(action.icon, context);
const resolvedChildren = resolve<Action[]>(action.children, context);
const visible = action.visible ? action.visible(context) : true;
@@ -67,6 +69,26 @@ export function actionToMenuItem(
};
}
if (action.to) {
return typeof action.to === "string"
? {
type: "route",
title,
icon,
visible,
to: action.to,
selected: action.selected?.(context),
}
: {
type: "link",
title,
icon,
visible,
href: action.to,
selected: action.selected?.(context),
};
}
return {
type: "button",
title,
@@ -113,9 +135,10 @@ export function actionToKBar(
shortcut: action.shortcut || [],
icon: resolvedIcon,
priority: (1 + (action.priority ?? 0)) * (1 + (sectionPriority ?? 0)),
perform: action.perform
? () => performAction(action, context)
: undefined,
perform:
action.perform || action.to
? () => performAction(action, context)
: undefined,
},
].concat(
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
@@ -124,7 +147,13 @@ export function actionToKBar(
}
export async function performAction(action: Action, context: ActionContext) {
const result = action.perform?.(context);
const result = action.perform
? action.perform(context)
: action.to
? typeof action.to === "string"
? history.push(action.to)
: window.open(action.to.url, action.to.target)
: undefined;
if (result instanceof Promise) {
return result.catch((err: Error) => {
+1 -1
View File
@@ -59,7 +59,7 @@ const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
disabled={disabled || executing}
ref={ref}
onClick={
action?.perform && actionContext
actionContext
? (ev) => {
ev.preventDefault();
ev.stopPropagation();
@@ -4,7 +4,6 @@ import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { documentPath } from "~/utils/routeHelpers";
const useRecentDocumentActions = (count = 6) => {
@@ -25,7 +24,7 @@ const useRecentDocumentActions = (count = 6) => {
) : (
<DocumentIcon />
),
perform: () => history.push(documentPath(item)),
to: documentPath(item),
})
),
[count, ui.activeDocumentId, documents.recentlyViewed]
@@ -3,7 +3,6 @@ import { useMemo } from "react";
import { createAction } from "~/actions";
import { NavigationSection } from "~/actions/sections";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import history from "~/utils/history";
const useSettingsAction = () => {
const config = useSettingsConfig();
@@ -16,7 +15,7 @@ const useSettingsAction = () => {
name: item.name,
icon: <Icon />,
section: NavigationSection,
perform: () => history.push(item.path),
to: item.path,
};
}),
[config]
+1 -1
View File
@@ -20,7 +20,7 @@ type Props = {
dangerous?: boolean;
to?: LocationDescriptor;
href?: string;
target?: "_blank";
target?: string;
as?: string | React.ComponentType<any>;
hide?: () => void;
level?: number;
+4 -2
View File
@@ -155,12 +155,14 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
return (
<MenuItem
id={`${item.title}-${index}`}
href={item.href}
href={typeof item.href === "string" ? item.href : item.href.url}
key={`${item.type}-${item.title}-${index}`}
disabled={item.disabled}
selected={item.selected}
level={item.level}
target={item.href.startsWith("#") ? undefined : "_blank"}
target={
typeof item.href === "string" ? undefined : item.href.target
}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
@@ -5,6 +5,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import useStores from "~/hooks/useStores";
import { fadeAndSlideUp } from "~/styles/animations";
import Notifications from "./Notifications";
@@ -14,9 +15,14 @@ type Props = {
const NotificationsPopover: React.FC = ({ children }: Props) => {
const { t } = useTranslation();
const { notifications } = useStores();
const scrollableRef = React.useRef<HTMLDivElement>(null);
const closeRef = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
void notifications.fetchPage({});
}, [notifications]);
const handleRequestClose = React.useCallback(() => {
if (closeRef.current) {
closeRef.current.click();
+1 -1
View File
@@ -25,7 +25,7 @@ const Error404 = () => {
</Trans>
</Empty>
<Flex gap={8}>
<Button action={navigateToHome} context={context} hideIcon>
<Button action={navigateToHome} context={context} neutral hideIcon>
{t("Home")}
</Button>
<Button action={navigateToSearch} context={context} neutral>
+2 -1
View File
@@ -65,7 +65,7 @@ export type MenuInternalLink = {
export type MenuExternalLink = {
type: "link";
title: React.ReactNode;
href: string;
href: string | { url: string; target?: string };
visible?: boolean;
selected?: boolean;
disabled?: boolean;
@@ -117,6 +117,7 @@ export type Action = {
* instead. Errors will be caught and displayed to the user as a toast message.
*/
perform?: (context: ActionContext) => any;
to?: string | { url: string; target?: string };
children?: ((context: ActionContext) => Action[]) | Action[];
};
+1 -1
View File
@@ -384,6 +384,6 @@
"qs": "6.9.7",
"prismjs": "1.30.0"
},
"version": "0.85.0",
"version": "0.85.1",
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+80
View File
@@ -97,4 +97,84 @@ describe("fetchOIDCConfiguration", () => {
"Missing authorization_endpoint in OIDC configuration"
);
});
it("should handle issuer URL with subdirectory path", async () => {
const mockConfig = {
issuer: "https://auth.example.com/application/o/outline/",
authorization_endpoint:
"https://auth.example.com/application/o/outline/auth",
token_endpoint: "https://auth.example.com/application/o/outline/token",
userinfo_endpoint:
"https://auth.example.com/application/o/outline/userinfo",
};
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
const result = await fetchOIDCConfiguration(
"https://auth.example.com/application/o/outline/"
);
expect(fetchMock).toHaveBeenCalledWith(
"https://auth.example.com/application/o/outline/.well-known/openid-configuration",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
Accept: "application/json",
}),
})
);
expect(result).toEqual(mockConfig);
});
it("should handle issuer URL with subdirectory path without trailing slash", async () => {
const mockConfig = {
issuer: "https://auth.example.com/application/o/outline",
authorization_endpoint:
"https://auth.example.com/application/o/outline/auth",
token_endpoint: "https://auth.example.com/application/o/outline/token",
userinfo_endpoint:
"https://auth.example.com/application/o/outline/userinfo",
};
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
const result = await fetchOIDCConfiguration(
"https://auth.example.com/application/o/outline"
);
expect(fetchMock).toHaveBeenCalledWith(
"https://auth.example.com/application/o/outline/.well-known/openid-configuration",
expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
Accept: "application/json",
}),
})
);
expect(result).toEqual(mockConfig);
});
it("should handle issuer URL that already contains well-known path", async () => {
const mockConfig = {
issuer: "https://example.com",
authorization_endpoint: "https://example.com/auth",
token_endpoint: "https://example.com/token",
userinfo_endpoint: "https://example.com/userinfo",
};
fetchMock.mockResponseOnce(JSON.stringify(mockConfig));
const result = await fetchOIDCConfiguration(
"https://example.com/.well-known/openid-configuration"
);
expect(fetchMock).toHaveBeenCalledWith(
"https://example.com/.well-known/openid-configuration",
expect.any(Object)
);
expect(result).toEqual(mockConfig);
});
});
+18 -3
View File
@@ -25,9 +25,24 @@ export async function fetchOIDCConfiguration(
): Promise<OIDCConfiguration> {
try {
const wellKnownPath = "/.well-known/openid-configuration";
const wellKnownUrl = issuerUrl.includes(wellKnownPath)
? issuerUrl
: new URL(wellKnownPath, issuerUrl).toString();
let wellKnownUrl: string;
// If the issuer URL already includes the well-known path, use it as-is
if (issuerUrl.includes(wellKnownPath)) {
wellKnownUrl = issuerUrl;
} else {
// Properly append well-known path to the issuer URL path
const url = new URL(issuerUrl);
// If pathname doesn't end with slash, append the full wellKnownPath (with leading slash)
if (!url.pathname.endsWith("/")) {
url.pathname += wellKnownPath;
} else {
// If pathname ends with slash, append wellKnownPath without leading slash
url.pathname += wellKnownPath.substring(1);
}
wellKnownUrl = url.toString();
}
Logger.info("plugins", `Fetching OIDC configuration from ${wellKnownUrl}`);
+1 -1
View File
@@ -19,6 +19,6 @@ export const TooManyConnections = {
};
export const EditorUpdateError = {
code: 5000,
code: 4999,
reason: "Editor Update Required",
};
+1 -1
View File
@@ -1,3 +1,3 @@
const EDITOR_VERSION = "14.0.0";
const EDITOR_VERSION = "15.0.0";
export default EDITOR_VERSION;