Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions static/app/bootstrap/processInitQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import throttle from 'lodash/throttle';

import {exportedGlobals} from 'sentry/bootstrap/exportGlobals';
import {CommandPaletteProvider} from 'sentry/components/commandPalette/context';
import {DocumentTitleManager} from 'sentry/components/sentryDocumentTitle/documentTitleManager';
import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider';
import {ScrapsProviders} from 'sentry/scrapsProviders';
import type {OnSentryInitConfiguration} from 'sentry/types/system';
Expand Down Expand Up @@ -111,17 +112,19 @@ async function processItem(initConfig: OnSentryInitConfiguration) {
* and so we dont know which theme to pick.
*/
<QueryClientProvider client={queryClient}>
<ThemeAndStyleProvider>
<CommandPaletteProvider>
<SimpleRouter
element={
<ScrapsProviders>
<Component {...props} />
</ScrapsProviders>
}
/>
</CommandPaletteProvider>
</ThemeAndStyleProvider>
<DocumentTitleManager>
<ThemeAndStyleProvider>
<CommandPaletteProvider>
<SimpleRouter
element={
<ScrapsProviders>
<Component {...props} />
</ScrapsProviders>
}
/>
</CommandPaletteProvider>
</ThemeAndStyleProvider>
</DocumentTitleManager>
</QueryClientProvider>
),
initConfig.container,
Expand Down
48 changes: 0 additions & 48 deletions static/app/components/sentryDocumentTitle.spec.tsx

This file was deleted.

86 changes: 0 additions & 86 deletions static/app/components/sentryDocumentTitle.tsx

This file was deleted.

69 changes: 69 additions & 0 deletions static/app/components/sentryDocumentTitle/documentTitleManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, {createContext, useContext, useEffect, useMemo, useState} from 'react';

const DEFAULT_PAGE_TITLE = 'Sentry';
const SEPARATOR = ' — ';

interface TitleEntry {
id: string;
noSuffix: boolean;
order: number;
text: string;
}

interface DocumentTitleManager {
register: (id: string, text: string, order: number, noSuffix: boolean) => void;
unregister: (id: string) => void;
}

const DocumentTitleContext = createContext<DocumentTitleManager>({
register: () => {},
unregister: () => {},
});

export const useDocumentTitleManager = () => useContext(DocumentTitleContext);

export function DocumentTitleManager({children}: React.PropsWithChildren) {
const [entries, setEntries] = useState<TitleEntry[]>([]);

const [manager] = useState<DocumentTitleManager>(() => ({
register: (id, text, order, noSuffix) => {
setEntries(prev => {
// update for same id
if (prev.some(e => e.id === id)) {
return prev.map(e => (e.id === id ? {...e, text, noSuffix} : e));
}
return [...prev, {id, text, noSuffix, order}];
});
},
unregister: id => {
setEntries(prev => prev.filter(e => e.id !== id));
},
}));

const fullTitle = useMemo(() => {
const entry = entries
.filter(e => e.text.trim() !== '')
.sort((a, b) => b.order - a.order)
.at(0);

const parts = entry ? [entry.text] : [];

if (!entry?.noSuffix) {
parts.push(DEFAULT_PAGE_TITLE);
}
return [...new Set([...parts])].join(SEPARATOR);
}, [entries]);

// write to the DOM title
useEffect(() => {
if (fullTitle.length > 0) {
document.title = fullTitle;
}
}, [fullTitle]);

return (
<DocumentTitleContext.Provider value={manager}>
{children}
</DocumentTitleContext.Provider>
);
}
71 changes: 71 additions & 0 deletions static/app/components/sentryDocumentTitle/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {render} from 'sentry-test/reactTestingLibrary';

import {DocumentTitleManager} from './documentTitleManager';
import SentryDocumentTitle from '.';

describe('SentryDocumentTitle', () => {
it('sets the document title', () => {
render(
<DocumentTitleManager>
<SentryDocumentTitle title="This is a test" />
</DocumentTitleManager>
);
expect(document.title).toBe('This is a test — Sentry');
});

it('adds a organization slug', () => {
render(
<DocumentTitleManager>
<SentryDocumentTitle orgSlug="org" title="This is a test" />
</DocumentTitleManager>
);
expect(document.title).toBe('This is a test — org — Sentry');
});

it('adds a project slug', () => {
render(
<DocumentTitleManager>
<SentryDocumentTitle projectSlug="project" title="This is a test" />
</DocumentTitleManager>
);
expect(document.title).toBe('This is a test — project — Sentry');
});

it('adds a organization and project slug', () => {
render(
<DocumentTitleManager>
<SentryDocumentTitle orgSlug="org" projectSlug="project" title="This is a test" />
</DocumentTitleManager>
);
expect(document.title).toBe('This is a test — org — project — Sentry');
});

it('sets the title without suffix', () => {
render(
<DocumentTitleManager>
<SentryDocumentTitle title="This is a test" noSuffix />
</DocumentTitleManager>
);
expect(document.title).toBe('This is a test');
});

it('reverts to the parent title', () => {
const {rerender} = render(
<DocumentTitleManager>
<SentryDocumentTitle title="This is a test">
<SentryDocumentTitle title="child title">Content</SentryDocumentTitle>
</SentryDocumentTitle>
</DocumentTitleManager>
);

expect(document.title).toBe('child title — Sentry');

rerender(
<DocumentTitleManager>
<SentryDocumentTitle title="This is a test">new Content</SentryDocumentTitle>
</DocumentTitleManager>
);

expect(document.title).toBe('This is a test — Sentry');
});
});
69 changes: 69 additions & 0 deletions static/app/components/sentryDocumentTitle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {useEffect, useId, useMemo, useState} from 'react';

import {useDocumentTitleManager} from './documentTitleManager';

type Props = {
children?: React.ReactNode;
/**
* Should the ` - Sentry` suffix be excluded?
*/
noSuffix?: boolean;
/**
* The organization slug to show in the title
*/
orgSlug?: string;
/**
* The project slug to show in the title.
*/
projectSlug?: string;

/**
* This string will be shown at the very front of the title
*/
title?: string;
};

function SentryDocumentTitle({
title = '',
orgSlug,
projectSlug,
noSuffix,
children,
}: Props) {
const titleManager = useDocumentTitleManager();
const id = useId();
// compute order once on mount because effects run bottom-up
const [order] = useState(() => performance.now());

const pageTitle = useMemo(() => {
if (orgSlug && projectSlug) {
return `${title} — ${orgSlug} — ${projectSlug}`;
}

if (orgSlug) {
return `${title} — ${orgSlug}`;
}

if (projectSlug) {
return `${title} — ${projectSlug}`;
}

return title;
}, [orgSlug, projectSlug, title]);

// create or update title entry
useEffect(() => {
titleManager.register(id, pageTitle, order, !!noSuffix);
}, [titleManager, id, pageTitle, order, noSuffix]);

// cleanup on unmount
useEffect(() => {
return () => {
titleManager.unregister(id);
};
}, [titleManager, id]);

return children;
}

export default SentryDocumentTitle;
Loading
Loading