Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add project and scope filters to checks side panel #1428

Merged
merged 18 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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 @@ -53,8 +53,15 @@
"%webView_checksSidePanel_focusedCheckDropdown_allowItem%": "Allow",
"%webView_checksSidePanel_focusedCheckDropdown_denyItem%": "Deny",
"%webView_checksSidePanel_focusedCheckDropdown_settingsItem%": "Open settings and inventories",
"%webView_checksSidePanel_loadingCheckResults%": "Loading Check Results",
"%webView_checksSidePanel_loadingCheckResults%": "Loading Check Results...",
"%webView_checksSidePanel_noCheckResults%": "No check results",
"%webView_checksSidePanel_projectFilter_label%": "Project",
"%webView_checksSidePanel_projectFilter_noProjectsFound%": "No projects found",
"%webView_checksSidePanel_projectFilter_noProjectSelected%": "No project",
"%webView_checksSidePanel_scopeFilter_all%": "All",
"%webView_checksSidePanel_scopeFilter_book%": "Book",
"%webView_checksSidePanel_scopeFilter_chapter%": "Chapter",
"%webView_checksSidePanel_scopeFilter_label%": "Scope",
"%webView_inventory_all%": "All items",
"%webView_inventory_approved%": "Approved items",
"%webView_inventory_unapproved%": "Unapproved items",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@papi/core';
import papi from '@papi/backend';
import checksSidePanelWebView from './checks-side-panel.web-view?inline';
import checksSidePanelWebViewStyles from './checks-side-panel.web-view.scss?inline';
import tailwindStyles from './tailwind.css?inline';

export const checksSidePanelWebViewType = 'platformScripture.checksSidePanel';

Expand All @@ -34,7 +34,7 @@ export default class ChecksSidePanelWebViewProvider implements IWebViewProvider
title,
projectId,
content: checksSidePanelWebView,
styles: checksSidePanelWebViewStyles,
styles: tailwindStyles,
scrollGroupScrRef: getWebViewOptions.editorScrollGroupId,
state: {
...savedWebView.state,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import {
SettableCheckDetails,
} from 'platform-scripture';
import { useData, useDataProvider, useLocalizedStrings } from '@papi/frontend/react';
import { VerseRef } from '@sillsdev/scripture';
import { LocalizeKey, ScriptureReference } from 'platform-bible-utils';
import { Canon, VerseRef } from '@sillsdev/scripture';
import { getChaptersForBook, LocalizeKey, ScriptureReference } from 'platform-bible-utils';
import { Spinner } from 'platform-bible-react';
import CheckCard, { CheckStates } from './checks/checks-side-panel/check-card.component';
import ChecksScopeFilter, {
CheckScopes,
} from './checks/configure-checks/checks-scope-filter.component';
import ChecksProjectFilter from './checks/configure-checks/checks-project-filter.component';

const defaultCheckRunnerCheckDetails: CheckRunnerCheckDetails = {
checkDescription: '',
Expand All @@ -32,14 +36,16 @@ const LOCALIZED_STRINGS: LocalizeKey[] = [
];

global.webViewComponent = function ChecksSidePanelWebView({
projectId,
projectId: editorProjectId,
useWebViewScrollGroupScrRef,
useWebViewState,
}: WebViewProps) {
const [scrRef, setScrRef, ,] = useWebViewScrollGroupScrRef();
const [selectedCheckId, setSelectedCheckId] = useState<string>('');
const [scope, setScope] = useState<CheckScopes>(CheckScopes.Chapter);
const [projectId, setProjectId] = useState(editorProjectId);
const [subscriptionId] = useWebViewState<CheckSubscriptionId>('subscriptionId', '');
const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRINGS);
const [localizedStrings] = useLocalizedStrings(useMemo(() => LOCALIZED_STRINGS, []));

const checkAggregator = useDataProvider('platformScripture.checkAggregator');

Expand Down Expand Up @@ -69,12 +75,27 @@ global.webViewComponent = function ChecksSidePanelWebView({
).IncludeDeniedResults(subscriptionId, true);

const checkInputRange: CheckInputRange = useMemo(() => {
// Default is chapter
let start = new VerseRef(scrRef.bookNum, scrRef.chapterNum, 1);
let end = new VerseRef(scrRef.bookNum, scrRef.chapterNum, 1);

if (scope === CheckScopes.Book) {
start = new VerseRef(scrRef.bookNum, 1, 1);
end = new VerseRef(scrRef.bookNum, getChaptersForBook(scrRef.bookNum), 1);
}

// TODO does all mean all books all chapters
if (scope === CheckScopes.All) {
start = new VerseRef(1, 1, 1);
end = new VerseRef(Canon.lastBook, getChaptersForBook(Canon.lastBook), 1);
}

return {
projectId: projectId ?? '',
start: new VerseRef(scrRef.bookNum, scrRef.chapterNum, 1),
end: new VerseRef(scrRef.bookNum, scrRef.chapterNum, 1),
start,
end,
};
}, [projectId, scrRef.bookNum, scrRef.chapterNum]);
}, [projectId, scope, scrRef]);

const settableCheckDetails: SettableCheckDetails = useMemo(() => {
return {
Expand Down Expand Up @@ -207,28 +228,53 @@ global.webViewComponent = function ChecksSidePanelWebView({
[checkAggregator, projectId],
);

const handleSelectProject = useCallback(
(newProjectId: string) => {
setProjectId(newProjectId);
},
[setProjectId],
);

const handleSelectScope = useCallback(
(newScope: CheckScopes) => {
setScope(newScope);
},
[setScope],
);

if (
isLoadingCheckResults ||
isLoadingAvailableChecks ||
isLoadingActiveRanges ||
isLoadingIncludeDeniedResults ||
!checkAggregator
)
) {
return (
<div className="pr-twp tw-h-screen tw-w-full tw-flex tw-flex-col tw-items-center tw-justify-center tw-gap-2">
<div className="pr-twp tw-box-border tw-h-screen tw-w-full tw-flex tw-flex-col tw-items-center tw-justify-center tw-gap-2">
<Spinner />
<span className="tw-text-sm">
{localizedStrings['%webView_checksSidePanel_loadingCheckResults%']}
</span>
</div>
);
}

return (
<div className="pr-twp">
<p>{subscriptionId}</p>
<div className="check-card-container">
<div className="pr-twp tw-p-3">
<div className="tw-flex tw-gap-1 tw-items-center tw-mb-2 tw-w-full tw-min-w-0">
<div className="tw-w-1/3 tw-min-w-0">
<ChecksProjectFilter
handleSelectProject={handleSelectProject}
selectedProjectId={projectId ?? ''}
/>
</div>
<div className="tw-w-2/3 tw-min-w-0">
<ChecksScopeFilter handleSelectScope={handleSelectScope} />
</div>
</div>
<div className="tw-flex tw-flex-col tw-justify-center tw-items-start tw-p-0 tw-gap-3">
{checkResults?.length === 0 ? (
<div className="tw-flex tw-flex-col tw-items-center tw-justify-center tw-h-screen tw-w-full">
<div className="tw-flex tw-flex-col tw-box-border tw-items-center tw-justify-center tw-h-screen tw-w-full">
<span className="tw-text-sm">
{localizedStrings['%webView_checksSidePanel_noCheckResults%']}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ export default function CheckCard({
<Card
onClick={() => handleSelectCheck(checkId)}
className={cn(
'pr-twp tw-w-full tw-flex tw-cursor-pointer tw-flex-col tw-items-flex-start tw-gap-3 tw-p-4 tw-rounded-lg hover:tw-shadow-xl tw-border-0',
{ 'tw-shadow-md': isSelected },
'pr-twp tw-w-full tw-flex tw-cursor-pointer tw-flex-col tw-items-flex-start tw-gap-3 tw-p-4 tw-rounded-lg hover:tw-shadow-md tw-border-0',
{ 'tw-shadow-sm': isSelected },
{ 'tw-bg-slate-100 tw-border-slate-100': !isSelected },
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import papi, { projectDataProviders } from '@papi/frontend';
import { useLocalizedStrings } from '@papi/frontend/react';
import { usePromise, RadioGroupItem, Label, RadioGroup } from 'platform-bible-react';
import { LocalizeKey } from 'platform-bible-utils';
import { useCallback, useMemo, useState } from 'react';
import FilterPopover from './filter-popover.component';

/** Props for ChecksProjectFilter component */
type ChecksProjectFilterProps = {
/** Callback function to handle the selection of a project. */
handleSelectProject: (projectId: string) => void;
/** The currently selected project ID. */
selectedProjectId: string;
};

/** Object containing strings for the project full and short names */
type ProjectOption = {
fullName: string;
shortName: string;
};

/**
* Gets the short and full names of a project from its ID.
*
* @param projectIdToGetName The ID of the project to get the names of.
* @returns An object with the short and full names of the project, or undefined if the project is
* not editable.
*/
async function getProjectNames(projectIdToGetName: string): Promise<ProjectOption | undefined> {
const pdp = await projectDataProviders.get('platform.base', projectIdToGetName);

if (!(await pdp.getSetting('platform.isEditable'))) return undefined;

const projectShortName = await pdp.getSetting('platform.name');
const projectFullName = await pdp.getSetting('platform.fullName');

return { shortName: projectShortName, fullName: projectFullName };
}

const LOCALIZED_STRINGS: LocalizeKey[] = [
'%webView_checksSidePanel_projectFilter_noProjectSelected%',
'%webView_checksSidePanel_projectFilter_noProjectsFound%',
'%webView_checksSidePanel_projectFilter_label%',
];

/** Dropdown component to select a project to run checks for */
export default function ChecksProjectFilter({
handleSelectProject,
selectedProjectId: selectedProjectIdFromWebView,
}: ChecksProjectFilterProps) {
const [selectedProjectId, setSelectedProjectId] = useState<string>(selectedProjectIdFromWebView);
const [localizedStrings] = useLocalizedStrings(useMemo(() => LOCALIZED_STRINGS, []));

const [projectIdsAndNames]: [{ [projectId: string]: ProjectOption }, boolean] = usePromise(
useCallback(async () => {
const projectDict: { [projectId: string]: ProjectOption } = {};

// Fetch projects metadata to get ids
const allMetadata = await papi.projectLookup.getMetadataForAllProjects();

// Map through all metadata to get ids and names
await Promise.all(
allMetadata.map(async (metadata) => {
const names = await getProjectNames(metadata.id);
if (!names) return;
projectDict[metadata.id] = names;
}),
);

return projectDict;
}, []),
useMemo(() => ({}), []),
);

const onProjectChange = useCallback(
(projectId: string) => {
setSelectedProjectId(projectId);
handleSelectProject(projectId);
},
[handleSelectProject],
);

const writeProjectName = useCallback((fullName: string, shortName: string) => {
return `${fullName} (${shortName})`;
}, []);

const getProjectShortNameLabel = useCallback(() => {
return (
projectIdsAndNames[selectedProjectId]?.shortName ??
localizedStrings['%webView_checksSidePanel_projectFilter_noProjectSelected%']
);
}, [localizedStrings, projectIdsAndNames, selectedProjectId]);

return (
<FilterPopover
selectedValue={selectedProjectId}
radioGroupLabel={localizedStrings['%webView_checksSidePanel_projectFilter_label%']}
getSelectedValueLabel={getProjectShortNameLabel}
shouldDisableButton
>
<RadioGroup value={selectedProjectId} onValueChange={onProjectChange} className="tw-p-3">
{Object.entries(projectIdsAndNames).length === 0
? localizedStrings['%webView_checksSidePanel_projectFilter_noProjectsFound%']
: Object.entries(projectIdsAndNames).map(([projectId, project]) => (
<div key={projectId} className="tw-flex tw-items-start tw-gap-2">
<RadioGroupItem value={projectId} id={projectId} className="tw-mt-0.5" />
<Label
htmlFor={projectId}
className="tw-flex-1 tw-text-sm tw-font-normal tw-leading-5"
>
{writeProjectName(project.fullName, project.shortName)}
</Label>
</div>
))}
</RadioGroup>
</FilterPopover>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useLocalizedStrings } from '@papi/frontend/react';
import { LocalizeKey } from 'platform-bible-utils';
import { useCallback, useMemo, useState } from 'react';
import { Label, RadioGroup, RadioGroupItem } from 'platform-bible-react';
import FilterPopover from './filter-popover.component';

/**
* Enum representing the different scopes that can be selected for checks.
*
* - `Chapter`: Scope of a single chapter.
* - `Book`: Scope of an entire book.
* - `All`: Scope of the entire project.
*/
export enum CheckScopes {
Chapter = 'Chapter',
Book = 'Book',
All = 'All',
/**
* Section of project text that the user can currently see. Not yet implemented, commented out so
* that it can still enumerate through these values
*/
// VisibleText = 'VisibleText',
}

type ChecksScopeFilterProps = {
/** Callback function to handle the selection of a scope. */
handleSelectScope: (scope: CheckScopes) => void;
};

const CHECK_SCOPE_FILTER_STRINGS: { [key in CheckScopes]: LocalizeKey } = {
Chapter: '%webView_checksSidePanel_scopeFilter_chapter%',
Book: '%webView_checksSidePanel_scopeFilter_book%',
All: '%webView_checksSidePanel_scopeFilter_all%',
};

const LOCALIZED_STRINGS: LocalizeKey[] = ['%webView_checksSidePanel_scopeFilter_label%'];

/**
* ChecksScopeFilter component provides a dropdown to select the scope for running checks. Users can
* choose between Chapter, Book, or All scopes. The component leverages popover and radio group
* elements for the UI and triggers a callback function when the scope changes.
*/
export default function ChecksScopeFilter({ handleSelectScope }: ChecksScopeFilterProps) {
const [selectedScope, setSelectedScope] = useState<CheckScopes>(CheckScopes.Chapter);
const [localizedStrings] = useLocalizedStrings(
useMemo(() => Object.values(CHECK_SCOPE_FILTER_STRINGS).concat(LOCALIZED_STRINGS), []),
);

const onScopeChange = useCallback(
(newScope: string) => {
// The scope is a string but we are using tighter types in this component
// eslint-disable-next-line no-type-assertion/no-type-assertion
const scope = newScope as CheckScopes;
setSelectedScope(scope);
handleSelectScope(scope);
},
[handleSelectScope],
);

const getScopeLabel = useCallback(
(scope: string) => {
// The scope is a string but we are using tighter types in this component
// eslint-disable-next-line no-type-assertion/no-type-assertion
return localizedStrings[CHECK_SCOPE_FILTER_STRINGS[scope as CheckScopes]];
},
[localizedStrings],
);

return (
<FilterPopover
selectedValue={selectedScope}
radioGroupLabel={localizedStrings['%webView_checksSidePanel_scopeFilter_label%']}
getSelectedValueLabel={getScopeLabel}
>
<RadioGroup value={selectedScope} onValueChange={onScopeChange} className="tw-p-3">
{Object.values(CheckScopes).map((scope) => (
<div key={scope} className="tw-flex tw-gap-2 tw-items-center">
<RadioGroupItem value={scope} id={scope} />
<Label htmlFor={scope} className="tw-flex-1 tw-text-sm tw-font-normal">
{getScopeLabel(scope)}
</Label>
</div>
))}
</RadioGroup>
</FilterPopover>
);
}
Loading
Loading