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

Create CSV export with only selected query rows #3476

Merged
merged 17 commits into from
May 24, 2023
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 @@ -166,7 +166,7 @@ export function QueryComboBox({
typeof resource.getDependentResource(field.name) === 'object')
? resource
.rgetPromise<string, AnySchema>(field.name)
.then((resource) =>
.then(async (resource) =>
resource === undefined || resource === null
? {
label: '' as LocalizedString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { PrintOnSave } from '../FormFields/Checkbox';
import type { ViewDescription } from '../FormParse';
import { SubViewContext } from '../Forms/SubView';
import { isTreeResource } from '../InitialContext/treeRanks';
import { interactionTables } from '../Interactions/config';
import { Dialog } from '../Molecules/Dialog';
import {
ProtectedAction,
Expand All @@ -33,7 +34,6 @@ import { QueryTreeUsages } from './QueryTreeUsages';
import { ReadOnlyMode } from './ReadOnlyMode';
import { ShareRecord } from './ShareRecord';
import { SubViewMeta } from './SubViewMeta';
import { interactionTables } from '../Interactions/config';

/**
* Form preferences host context aware user preferences and other meta-actions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ function RecordSet<SCHEMA extends AnySchema>({
}).then(({ totalCount }) => totalCount !== 0),
})
)
).then((results) => {
).then(async (results) => {
const [nonDuplicates, duplicates] = split(
results,
({ isDuplicate }) => isDuplicate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ function TableResult({
[fieldSpecs]
);

const [selectedRows, setSelectedRows] = React.useState<ReadonlySet<number>>(
new Set()
);

return (
<details>
<summary
Expand Down Expand Up @@ -236,6 +240,7 @@ function TableResult({
label={model.label}
model={model}
queryResource={undefined}
selectedRows={[selectedRows, setSelectedRows]}
tableClassName="max-h-[70vh]"
totalCount={tableResults.totalCount}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { f } from '../../utils/functools';
import type { RA } from '../../utils/types';
import { ensure } from '../../utils/types';
import type { StatLayout } from '../Statistics/types';
import { GenericPreferences, defineItem } from './types';
import type { GenericPreferences } from './types';
import { defineItem } from './types';

export const collectionPreferenceDefinitions = {
statistics: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';

import { useLiveState } from '../../hooks/useLiveState';
import type { AppResourceTab } from '../AppResources/TabDefinitions';
import { PreferencesContent } from '../Preferences';
import { BasePreferences } from '../Preferences/BasePreferences';
import { userPreferenceDefinitions } from '../Preferences/UserDefinitions';
import { userPreferences } from '../Preferences/userPreferences';
import { AppResourceTab } from '../AppResources/TabDefinitions';
import { useLiveState } from '../../hooks/useLiveState';

export const UserPreferencesEditor: AppResourceTab = function ({
isReadOnly,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import { rawMenuItemsPromise } from '../Header/menuItemDefinitions';
import { useMenuItems, useUserTools } from '../Header/menuItemProcessing';
import { AttachmentPicker } from '../Molecules/AttachmentPicker';
import { AutoComplete } from '../Molecules/AutoComplete';
import { userPreferences } from './userPreferences';
import { ListEdit } from '../Toolbar/QueryTablesEdit';
import { PreferenceItem, PreferenceItemComponent } from './types';
import type { PreferenceItem, PreferenceItemComponent } from './types';
import { userPreferences } from './userPreferences';

export const ColorPickerPreferenceItem: PreferenceItemComponent<string> =
function ColorPickerPreferenceItem({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ export function MakeRecordSetButton({
setState('editing');
if (typeof getQueryFieldRecords === 'function')
queryResource.set('fields', getQueryFieldRecords());

const recordSet = new schema.models.RecordSet.Resource();

if (!queryResource.isNew())
Expand Down
64 changes: 54 additions & 10 deletions specifyweb/frontend/js_src/lib/components/QueryBuilder/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,32 @@ import React from 'react';
import { commonText } from '../../localization/common';
import { queryText } from '../../localization/query';
import { ping } from '../../utils/ajax/ping';
import { f } from '../../utils/functools';
import type { RA } from '../../utils/types';
import { keysToLowerCase } from '../../utils/utils';
import type { SerializedResource } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { schema } from '../DataModel/schema';
import type { SpQuery, SpQueryField, Tables } from '../DataModel/types';
import { Dialog } from '../Molecules/Dialog';
import { downloadFile } from '../Molecules/FilePicker';
import { hasPermission } from '../Permissions/helpers';
import { userPreferences } from '../Preferences/userPreferences';
import { mappingPathIsComplete } from '../WbPlanView/helpers';
import { generateMappingPathPreview } from '../WbPlanView/mappingPreview';
import { QueryButton } from './Components';
import type { QueryField } from './helpers';
import { hasLocalityColumns } from './helpers';
import type { QueryResultRow } from './Results';

export function QueryExportButtons({
baseTableName,
fields,
queryResource,
getQueryFieldRecords,
recordSetId,
results,
selectedRows,
}: {
readonly baseTableName: keyof Tables;
readonly fields: RA<QueryField>;
Expand All @@ -31,6 +37,10 @@ export function QueryExportButtons({
| (() => RA<SerializedResource<SpQueryField>>)
| undefined;
readonly recordSetId: number | undefined;
readonly results: React.MutableRefObject<
RA<QueryResultRow | undefined> | undefined
>;
readonly selectedRows: ReadonlySet<number>;
}): JSX.Element {
const showConfirmation = (): boolean =>
fields.some(({ mappingPath }) => !mappingPathIsComplete(mappingPath));
Expand Down Expand Up @@ -59,6 +69,37 @@ export function QueryExportButtons({
});
}

const [separator] = userPreferences.use(
'queryBuilder',
'behavior',
'exportFileDelimiter'
);

/*
*Will be only called if query is not distinct,
*selection not enabled when distinct selected
*/
function handleSelectedResults(): string {
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
const selectedResults = results?.current?.filter((item) =>
f.has(selectedRows, item?.[0])
);

const joinedSelected = selectedResults?.map((subArray) =>
subArray?.slice(1).join(separator)
);

const resultToExport = [
fields
.map((field) =>
generateMappingPathPreview(baseTableName, field.mappingPath)
)
.join(separator),
...(joinedSelected ?? []),
];

return resultToExport.join('\n');
}

const canUseKml =
(baseTableName === 'Locality' ||
fields.some(({ mappingPath }) => mappingPath.includes('locality'))) &&
Expand Down Expand Up @@ -87,16 +128,19 @@ export function QueryExportButtons({
<QueryButton
disabled={fields.length === 0}
showConfirmation={showConfirmation}
onClick={(): void =>
doQueryExport(
'/stored_query/exportcsv/',
userPreferences.get(
'queryBuilder',
'behavior',
'exportFileDelimiter'
)
)
}
onClick={(): void => {
selectedRows.size === 0
? doQueryExport('/stored_query/exportcsv/', separator)
: downloadFile(
`${
queryResource.isNew()
? `${queryText.newQueryName()} -
${schema.models[baseTableName].label}`
: queryResource.get('name')
} - ${new Date().toDateString()}.csv`,
handleSelectedResults()
);
}}
>
{queryText.createCsv()}
</QueryButton>
Expand Down
16 changes: 10 additions & 6 deletions specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ type Props = {
readonly createRecordSet: JSX.Element | undefined;
readonly extraButtons: JSX.Element | undefined;
readonly tableClassName?: string;
readonly selectedRows: GetSet<ReadonlySet<number>>;
readonly resultsRef?: React.MutableRefObject<
RA<QueryResultRow | undefined> | undefined
>;
};

export function QueryResults(props: Props): JSX.Element {
Expand All @@ -86,6 +90,8 @@ export function QueryResults(props: Props): JSX.Element {
createRecordSet,
extraButtons,
tableClassName = '',
selectedRows: [selectedRows, setSelectedRows],
resultsRef,
} = props;
const visibleFieldSpecs = fieldSpecs.filter(({ isPhantom }) => !isPhantom);

Expand All @@ -97,12 +103,14 @@ export function QueryResults(props: Props): JSX.Element {
canFetchMore,
} = useFetchQueryResults(props);

if (resultsRef !== undefined) resultsRef.current = results;

const [pickListsLoaded = false] = useAsyncState(
React.useCallback(
async () =>
// Fetch all pick lists so that they are accessible synchronously later
Promise.all(
fieldSpecs.map((fieldSpec) =>
fieldSpecs.map(async (fieldSpec) =>
typeof fieldSpec.parser.pickListName === 'string'
? fetchPickList(fieldSpec.parser.pickListName)
: undefined
Expand All @@ -120,10 +128,6 @@ export function QueryResults(props: Props): JSX.Element {

const [treeRanksLoaded = false] = useAsyncState(fetchTreeRanks, false);

// Ids of selected records
const [selectedRows, setSelectedRows] = React.useState<ReadonlySet<number>>(
new Set()
);
const lastSelectedRow = React.useRef<number | undefined>(undefined);
// Unselect all rows when query is reRun
React.useEffect(() => setSelectedRows(new Set()), [fieldSpecs]);
Expand Down Expand Up @@ -404,7 +408,7 @@ export function useFetchQueryResults({

// Prevent concurrent fetching in different places
fetchersRef.current[fetchIndex] ??= fetchResults(fetchIndex)
.then((newResults) => {
.then(async (newResults) => {
if (
process.env.NODE_ENV === 'development' &&
newResults.length > fetchSize
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { ajax } from '../../utils/ajax';
import type { IR, RA } from '../../utils/types';
import type { GetSet, IR, RA } from '../../utils/types';
import { keysToLowerCase, replaceItem } from '../../utils/utils';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import type { SpecifyModel } from '../DataModel/specifyModel';
Expand Down Expand Up @@ -69,6 +69,10 @@ type ResultsProps = {
*/
newFields: RA<QueryField>
) => void;
readonly selectedRows: GetSet<ReadonlySet<number>>;
readonly resultsRef?: React.MutableRefObject<
RA<QueryResultRow | undefined> | undefined
>;
};

type PartialProps = Omit<
Expand Down Expand Up @@ -99,13 +103,15 @@ export function useQueryResultsWrapper({
recordSetId,
forceCollection,
onSortChange: handleSortChange,
selectedRows: [selectedRows, setSelectedRows],
resultsRef,
}: ResultsProps): PartialProps | undefined {
/*
* Need to store all props in a state so that query field edits do not affect
* the query results until query is reRun
*/
const [props, setProps] = React.useState<
Omit<PartialProps, 'totalCount'> | undefined
Omit<PartialProps, 'resultsRef' | 'selectedRows' | 'totalCount'> | undefined
>(undefined);

const [totalCount, setTotalCount] = React.useState<number | undefined>(
Expand Down Expand Up @@ -217,5 +223,7 @@ export function useQueryResultsWrapper({
: {
...props,
totalCount,
selectedRows: [selectedRows, setSelectedRows],
resultsRef,
};
}
13 changes: 13 additions & 0 deletions specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { QueryFromMap } from './FromMap';
import { QueryHeader } from './Header';
import { mutateLineData, smoothScroll, unParseQueryFields } from './helpers';
import { getInitialState, reducer } from './reducer';
import type { QueryResultRow } from './Results';
import { QueryResultsWrapper } from './ResultsWrapper';
import { QueryToolbar } from './Toolbar';

Expand Down Expand Up @@ -98,6 +99,10 @@ export function QueryBuilder({
const [query, setQuery] = useResource(queryResource);
useErrorContext('query', query);

const [selectedRows, setSelectedRows] = React.useState<ReadonlySet<number>>(
new Set()
);

const model = getModelById(query.contextTableId);
const buildInitialState = React.useCallback(
() =>
Expand Down Expand Up @@ -248,6 +253,10 @@ export function QueryBuilder({
);
const resultsShown = state.queryRunCount !== 0;

const resultsRef = React.useRef<RA<QueryResultRow | undefined> | undefined>(
undefined
);

return treeRanksLoaded ? (
<Container.Full
className={`overflow-hidden ${isEmbedded ? 'py-0' : ''}`}
Expand Down Expand Up @@ -537,6 +546,8 @@ export function QueryBuilder({
getQueryFieldRecords={getQueryFieldRecords}
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
queryResource={queryResource}
recordSetId={recordSet?.id}
results={resultsRef}
selectedRows={selectedRows}
/>
)
}
Expand All @@ -546,6 +557,8 @@ export function QueryBuilder({
queryResource={queryResource}
queryRunCount={state.queryRunCount}
recordSetId={recordSet?.id}
resultsRef={resultsRef}
selectedRows={[selectedRows, setSelectedRows]}
onSelected={handleSelected}
onSortChange={(fields): void => {
dispatch({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

import { commonText } from '../../localization/common';
import { headerText } from '../../localization/header';
import { interactionsText } from '../../localization/interactions';
Expand All @@ -8,9 +10,8 @@ import { userText } from '../../localization/user';
import { welcomeText } from '../../localization/welcome';
import { wbText } from '../../localization/workbench';
import type { RA } from '../../utils/types';
import type { EnhancedRoute } from './RouterUtils';
import { Redirect } from './Redirect';
import React from 'react';
import type { EnhancedRoute } from './RouterUtils';

/* eslint-disable @typescript-eslint/promise-function-async */
/**
Expand Down
4 changes: 2 additions & 2 deletions specifyweb/frontend/js_src/lib/components/Security/User.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ function UserView({
status: Http.NO_CONTENT,
})
)
.then(({ data, status }) =>
.then(async ({ data, status }) =>
status === Http.BAD_REQUEST
? setState({
type: 'SettingAgents',
Expand Down Expand Up @@ -515,7 +515,7 @@ function UserView({
})
: true
)
.then((canContinue) =>
.then(async (canContinue) =>
canContinue === true
? Promise.all([
typeof password === 'string' && password !== ''
Expand Down
Loading