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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const createStackOverflowApi = (baseUrl: string) => {

const headers: Record<string, string> = {
'Content-Type': 'application/json',
'User-Agent': 'SOBackstage-Plugin',
Authorization: `Bearer ${authToken}`,
};

Expand Down
3 changes: 1 addition & 2 deletions plugins/stack-overflow-teams-backend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export async function createRouter({
try {
const baseUrl = stackOverflowConfig.baseUrl;
const teamsAPIUrl = 'https://api.stackoverflowteams.com';
const teamsBaseUrl = `https://stackoverflowteams.com/c/${stackOverflowConfig.teamName}`; // Fixed URL to match your previous code
const teamsBaseUrl = `https://stackoverflowteams.com/c/${stackOverflowConfig.teamName}`;

if (baseUrl === teamsAPIUrl) {
return res.json({ SOInstance: teamsBaseUrl, teamName: stackOverflowConfig.teamName });
Expand Down Expand Up @@ -296,7 +296,6 @@ export async function createRouter({
}
});

// Updated questions route with filtering support
router.get('/questions', async (req: Request, res: Response) => {
try {
const authToken = getValidAuthToken(req, res);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ export const StackOverflowPostQuestionModal = () => {
</Alert>
)}

<Box mt={3} sx={{ borderTop: 1, borderColor: 'divider', pt: 2, display: 'flex', gap: 2 }}>
<Box my={3} sx={{ borderTop: 1, borderColor: 'divider', pt: 2, display: 'flex', gap: 2 }}>
<Button
variant="contained"
className={classes.button}
Expand Down Expand Up @@ -793,7 +793,7 @@ export const StackOverflowPostQuestionModal = () => {
<Grid item xs={12} md={8}>
{renderQuestionForm()}
</Grid>
<Grid item xs={12} md={4}>
<Grid item xs={12} md={4} >
{renderRightPanel()}
</Grid>
</Grid>
Expand All @@ -808,7 +808,7 @@ export const StackOverflowPostQuestionModal = () => {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '95vw', sm: '90vw', md: '80vw', lg: '70vw' },
width: { xs: '95vw', sm: '90vw', md: '80vw', lg: '80vw', xl: '70vw' },
maxHeight: '90vh',
bgcolor: 'background.paper',
boxShadow: 24,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ import {
Box,
} from '@material-ui/core';
import Skeleton from '@mui/material/Skeleton';
import { Link, ResponseErrorPanel } from '@backstage/core-components';
import { ResponseErrorPanel } from '@backstage/core-components';
import { useStackOverflowSearch } from './hooks';
import { useStackOverflowData } from './hooks';
import { StackOverflowSearchResultListItem } from './StackOverflowSearchResultListItem';
import SearchIcon from '@material-ui/icons/Search';
import { StackOverflowIcon } from '../../icons';
import { useApi } from '@backstage/core-plugin-api';
import { stackoverflowteamsApiRef } from '../../api';

const useStyles = makeStyles((theme: Theme) => ({
filters: {
Expand Down Expand Up @@ -190,7 +188,6 @@ const useEnhancedSearch = () => {
return;
}

// If we have cached data for this server page, don't refetch
if (searchCache[cacheKey]) {
return;
}
Expand Down Expand Up @@ -269,10 +266,7 @@ const useEnhancedSearch = () => {

export const StackOverflowQuestions = () => {
const classes = useStyles();
const stackOverflowTeamsApi = useApi(stackoverflowteamsApiRef);
const [baseUrl, setBaseUrl] = useState<string>('');

// Questions data using enhanced hook
const {
data: questionsData,
loading: questionsLoading,
Expand All @@ -283,15 +277,13 @@ export const StackOverflowQuestions = () => {
fetchUnansweredQuestions
} = useStackOverflowData('questions');

// Enhanced search hook
const {
enhancedSearch,
getSearchDisplayData,
clearSearch,
clearSearchCache,
} = useEnhancedSearch();

// State management
const [searchTerm, setSearchTerm] = useState('');
const [activeFilter, setActiveFilter] = useState<string>('active');
const [currentPage, setCurrentPage] = useState(1);
Expand All @@ -310,16 +302,10 @@ export const StackOverflowQuestions = () => {
clearSearchCacheRef.current = clearSearchCache;
});

// Debounce search term
const debouncedSearchTerm = useDebounce(searchTerm, 500);

// Determine if we're in search mode
const isSearchMode = !!searchTerm.trim();

useEffect(() => {
stackOverflowTeamsApi.getBaseUrl().then(url => setBaseUrl(url));
}, [stackOverflowTeamsApi]);

// Calculate required server page for current client page (only for questions)
const requiredServerPage = useMemo(() => {
return calculateServerPage(questionsPage, SERVER_ITEMS_PER_PAGE);
Expand Down Expand Up @@ -523,10 +509,9 @@ export const StackOverflowQuestions = () => {
/>
</Box>

{/* Top pagination controls - always visible */}
<PaginationControls />

{/* Only show filters when not in search mode - always visible */}
{/* Only show filters when not in search mode */}
{!isSearchMode && (
<Paper className={classes.filters}>
<ButtonGroup className={classes.buttonGroup}>
Expand All @@ -550,28 +535,19 @@ export const StackOverflowQuestions = () => {
: `Showing ${displayInfo.currentPageData.length} of ${displayInfo.totalCount} results`}
</Typography>

{/* Show loading skeleton for initial loads and when switching pages */}
{displayInfo.loading && <LoadingSkeleton />}

{/* Show content only when not loading */}
{!displayInfo.loading && (
<>
{/* No results */}
{displayInfo.currentPageData.length === 0 && (
{displayInfo.currentPageData && displayInfo.totalCount === 0 && (
<Box textAlign="center" py={4}>
<Typography variant="body1" gutterBottom>
{searchTerm.trim()
? `No questions found matching "${searchTerm}"`
: "No questions found"
}
</Typography>
{searchTerm.trim() && (
<Link
to={`${baseUrl}/search?q=${encodeURIComponent(searchTerm)}`}
>
However, you might find more questions on your Stack Overflow Team.
</Link>
)}
</Box>
)}

Expand Down Expand Up @@ -603,23 +579,11 @@ export const StackOverflowQuestions = () => {
</Grid>
))}
</Grid>

<Box mt={2}>
<Link to={searchTerm.trim()
? `${baseUrl}/search?q=${encodeURIComponent(searchTerm)}`
: `${baseUrl}/questions`}
>
<Typography variant='body1'>
Explore more questions on your Stack Overflow Team
</Typography>
</Link>
</Box>
</>
)}
</>
)}

{/* Bottom pagination controls - always visible */}
<PaginationControls />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export const StackOverflowSearchResultListItem = (
}
/>

{/* Author section - moved outside of ListItemText to avoid nested links */}
<Box display="flex" alignItems="center" mt={1}>
{/* Author Avatar */}
{result.avatar && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { Link, Progress, ResponseErrorPanel } from '@backstage/core-components';
import { useStackOverflowData } from './hooks/';
import {
Expand All @@ -17,23 +17,28 @@ import { useApi } from '@backstage/core-plugin-api';
const StackOverflowTagList = ({
tags,
searchTerm,
baseUrl, // Use the baseUrl prop
isApiSearch = false,
isSearching = false,
}: {
tags: Tag[];
searchTerm: string;
baseUrl: string;
isApiSearch?: boolean;
isSearching?: boolean;
}) => {
if (isSearching) {
return null;
}

if (tags.length === 0) {
return (
<Box textAlign="center" py={4}>
<Typography variant="body1" gutterBottom>
No matching tags were found for "{searchTerm}"
{isApiSearch
? `No tags found for "${searchTerm}" on your Stack Overflow Team`
: `No matching tags were found for "${searchTerm}"`
}
</Typography>
<Link
to={`${baseUrl}/tags?query=${encodeURIComponent(searchTerm)}`}
>
However, you might find this tag on your Stack Overflow Team.
</Link>
</Box>
);
}
Expand All @@ -49,40 +54,91 @@ const StackOverflowTagList = ({
/>
</Link>
))}

<Grid item xs={12}>
<Link to={ searchTerm ? `${baseUrl}/tags?query=${encodeURIComponent(searchTerm)}` :`${baseUrl}/tags`}>
<Typography variant='body1'>
Explore more tags on your Stack Overflow Team
</Typography>
</Link>
</Grid>
</Grid>
);
};

export const StackOverflowTags = () => {
const { data, loading, error } = useStackOverflowData('tags');
const { data, loading, error, fetchData } = useStackOverflowData('tags');
const [searchTerm, setSearchTerm] = useState('');
const [apiSearchResults, setApiSearchResults] = useState<Tag[] | null>(null);
const [apiSearchLoading, setApiSearchLoading] = useState(false);
const [apiSearchError, setApiSearchError] = useState<Error | null>(null);
const [hasAttemptedApiSearch, setHasAttemptedApiSearch] = useState(false);
const stackOverflowTeamsApi = useApi(stackoverflowteamsApiRef);

const [baseUrl, setBaseUrl] = useState<string>('');

useEffect(() => {
stackOverflowTeamsApi.getBaseUrl().then(url => setBaseUrl(url));
}, [stackOverflowTeamsApi]);

if (loading) {
return <Progress />;
}
useEffect(() => {
fetchData();
}, [fetchData]);

if (error) {
return <ResponseErrorPanel error={error} />;
}
// Debounced API search function
const searchTagsViaApi = useCallback(async (searchQuery: string) => {
if (!searchQuery.trim()) {
setApiSearchResults(null);
setHasAttemptedApiSearch(false);
return;
}

const filteredTags = (data?.tags || []).filter(tag =>
setApiSearchLoading(true);
setApiSearchError(null);
setHasAttemptedApiSearch(false);

try {
const response = await stackOverflowTeamsApi.getTags(searchQuery);
setApiSearchResults(response.items || []);
} catch (err) {
setApiSearchError(err as Error);
setApiSearchResults([]);
} finally {
setApiSearchLoading(false);
setHasAttemptedApiSearch(true);
}
}, [stackOverflowTeamsApi]);

// Debounce the search to avoid too many API calls
useEffect(() => {
const timer = setTimeout(() => {
const localFilteredTags = (data?.tags || []).filter(tag =>
tag.name.toLowerCase().includes(searchTerm.toLowerCase()),
);

// If no local results and we have a search term, try API search
if (localFilteredTags.length === 0 && searchTerm.trim()) {
searchTagsViaApi(searchTerm);
} else {
// Reset API search results when we have local results or no search term
setApiSearchResults(null);
setApiSearchError(null);
setHasAttemptedApiSearch(false);
}
}, 300); // 300ms debounce

return () => clearTimeout(timer);
}, [searchTerm, data?.tags, searchTagsViaApi]);

const localFilteredTags = (data?.tags || []).filter(tag =>
tag.name.toLowerCase().includes(searchTerm.toLowerCase()),
);

const shouldShowApiResults = localFilteredTags.length === 0 && searchTerm.trim() && hasAttemptedApiSearch && apiSearchResults !== null;
const tagsToShow = shouldShowApiResults ? apiSearchResults : localFilteredTags;
const isShowingApiResults = shouldShowApiResults;
const currentLoading = apiSearchLoading;
const currentError = apiSearchError;

const shouldShowResults = !currentLoading && !currentError && (
// Show local results if we have them
localFilteredTags.length > 0 ||
// Show API results if we have attempted API search and got results
(hasAttemptedApiSearch && apiSearchResults !== null)
);

return (
<div>
<Box mb={2}>
Expand All @@ -102,13 +158,35 @@ export const StackOverflowTags = () => {
/>
</Box>

<StackOverflowTagList
tags={filteredTags}
searchTerm={searchTerm}
baseUrl={baseUrl}
/>
{/* Show initial loading state */}
{loading && <Progress />}

{/* Show initial error state */}
{error && <ResponseErrorPanel error={error} />}

{/* Only show content when initial data is loaded */}
{!loading && !error && (
<>
{/* Show API search loading */}
{currentLoading && <Progress />}

{/* Show API search error */}
{currentError && <ResponseErrorPanel error={currentError} />}

{/* Show results when appropriate */}
{shouldShowResults && (
<StackOverflowTagList
tags={tagsToShow || []}
searchTerm={searchTerm}
baseUrl={baseUrl}
isApiSearch={!!isShowingApiResults}
isSearching={currentLoading}
/>
)}
</>
)}
</div>
);
};

export default StackOverflowTags;
export default StackOverflowTags;
Loading