From 336a387a881cfa65a0a1ae781f06dd39f7cf5ffd Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Mon, 11 Nov 2024 21:37:01 -0800
Subject: [PATCH 01/15] Upgrade regex to cover replaces
---
packages/core/services/DatabaseService/index.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/packages/core/services/DatabaseService/index.ts b/packages/core/services/DatabaseService/index.ts
index ec6d82c7..c8f2b1dd 100644
--- a/packages/core/services/DatabaseService/index.ts
+++ b/packages/core/services/DatabaseService/index.ts
@@ -326,11 +326,8 @@ export default abstract class DatabaseService {
// Grab near matches to the pre-defined columns like "file_name" for "File Name"
const matches = [...columnsOnDataSource].filter((column) => {
const simplifiedColumn = column
- .trim()
.toLowerCase() // File Name -> file name
- .replace("_", "") // file_path -> filepath
- .replace(" ", "") // file path -> filepath
- .replace("-", ""); // file-path -> filepath
+ .replaceAll(/\s|-|_/g, ""); // Matches any whitespace, hyphen, or underscore
return simplifiedColumn === preDefinedColumnSimplified;
});
From ea6a10de512f99f3102b500c218a0e57f75c38f7 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Mon, 11 Nov 2024 21:38:47 -0800
Subject: [PATCH 02/15] Add missing await
---
packages/core/services/DatabaseService/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/services/DatabaseService/index.ts b/packages/core/services/DatabaseService/index.ts
index c8f2b1dd..f03dbfcb 100644
--- a/packages/core/services/DatabaseService/index.ts
+++ b/packages/core/services/DatabaseService/index.ts
@@ -343,7 +343,7 @@ export default abstract class DatabaseService {
if (matches.length === 1) {
// Rename matching column name to new pre-defined column
- this.execute(`
+ await this.execute(`
ALTER TABLE "${dataSourceName}"
RENAME COLUMN "${matches[0]}" TO '${preDefinedColumn}'
`);
From 71c3537b2f014386b274ffdfa61aa3c1690595ec Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Mon, 11 Nov 2024 21:52:43 -0800
Subject: [PATCH 03/15] Use enum for pre-defined columns
---
.../core/services/DatabaseService/index.ts | 71 ++++++++++---------
1 file changed, 37 insertions(+), 34 deletions(-)
diff --git a/packages/core/services/DatabaseService/index.ts b/packages/core/services/DatabaseService/index.ts
index f03dbfcb..133be927 100644
--- a/packages/core/services/DatabaseService/index.ts
+++ b/packages/core/services/DatabaseService/index.ts
@@ -7,14 +7,15 @@ import { Source } from "../../entity/FileExplorerURL";
import SQLBuilder from "../../entity/SQLBuilder";
import DataSourcePreparationError from "../../errors/DataSourcePreparationError";
-const PRE_DEFINED_COLUMNS = [
- "File ID",
- "File Path",
- "File Name",
- "File Size",
- "Thumbnail",
- "Uploaded",
-];
+enum PreDefinedColumn {
+ FILE_ID = "File ID",
+ FILE_PATH = "File Path",
+ FILE_NAME = "File Name",
+ FILE_SIZE = "File Size",
+ THUMBNAIL = "Thumbnail",
+ UPLOADED = "Uploaded",
+}
+const PRE_DEFINED_COLUMNS = Object.values(PreDefinedColumn);
/**
* Service reponsible for querying against a database
@@ -167,10 +168,10 @@ export default abstract class DatabaseService {
*/
protected async addRequiredColumns(name: string): Promise {
const dataSourceColumns = await this.getColumnsOnDataSource(name);
- if (!dataSourceColumns.has("File ID")) {
+ if (!dataSourceColumns.has(PreDefinedColumn.FILE_ID)) {
await this.execute(`
ALTER TABLE "${name}"
- ADD COLUMN IF NOT EXISTS "File ID" VARCHAR;
+ ADD COLUMN IF NOT EXISTS "${PreDefinedColumn.FILE_ID}" VARCHAR;
`);
// The data source's input date gets appended to the end
// of the auto-generated name, that makes it kind of messy
@@ -180,33 +181,33 @@ export default abstract class DatabaseService {
// ordered by file path.
await this.execute(`
UPDATE "${name}"
- SET "File ID" = CONCAT(SQ.row, '-', '${dataSourceNameWithoutDate}')
+ SET "${PreDefinedColumn.FILE_ID}" = CONCAT(SQ.row, '-', '${dataSourceNameWithoutDate}')
FROM (
- SELECT "File Path", ROW_NUMBER() OVER (ORDER BY "File Path") AS row
+ SELECT "${PreDefinedColumn.FILE_PATH}", ROW_NUMBER() OVER (ORDER BY "${PreDefinedColumn.FILE_PATH}") AS row
FROM "${name}"
) AS SQ
- WHERE "${name}"."File Path" = SQ."File Path";
+ WHERE "${name}"."${PreDefinedColumn.FILE_PATH}" = SQ."${PreDefinedColumn.FILE_PATH}";
`);
}
- if (!dataSourceColumns.has("File Name")) {
+ if (!dataSourceColumns.has(PreDefinedColumn.FILE_NAME)) {
await this.execute(`
ALTER TABLE "${name}"
- ADD COLUMN IF NOT EXISTS "File Name" VARCHAR;
+ ADD COLUMN IF NOT EXISTS "${PreDefinedColumn.FILE_NAME}" VARCHAR;
`);
// Best shot attempt at auto-generating a "File Name"
// from the "File Path", defaults to full path if this fails
await this.execute(`
UPDATE "${name}"
- SET "File Name" = COALESCE(
+ SET "${PreDefinedColumn.FILE_NAME}" = COALESCE(
NULLIF(
REGEXP_REPLACE(
- "File Path",
+ "${PreDefinedColumn.FILE_PATH}",
'^.*/([^/]*?)(\\.[^/.]+)?$', '\\1',
''
),
''),
- "File Path");
+ "${PreDefinedColumn.FILE_PATH}");
`);
}
@@ -224,21 +225,24 @@ export default abstract class DatabaseService {
const columnsOnTable = await this.getColumnsOnDataSource(name);
// If a data source has a File ID it must also pass validation
- const hasFileIdColumn = columnsOnTable.has("File ID");
+ const hasFileIdColumn = columnsOnTable.has(PreDefinedColumn.FILE_ID);
if (hasFileIdColumn) {
// Check for empty or just whitespace File ID column values
- const blankFileIdRows = await this.getRowsWhereColumnIsBlank(name, "File ID");
+ const blankFileIdRows = await this.getRowsWhereColumnIsBlank(
+ name,
+ PreDefinedColumn.FILE_ID
+ );
if (blankFileIdRows.length > 0) {
const rowNumbers = DatabaseService.truncateString(blankFileIdRows.join(", "), 100);
errors.push(
- `"File ID" column contains ${blankFileIdRows.length} empty or purely whitespace values at rows ${rowNumbers}.`
+ `"${PreDefinedColumn.FILE_ID}" column contains ${blankFileIdRows.length} empty or purely whitespace ids at rows ${rowNumbers}.`
);
}
// Check for duplicate File ID column values
const duplicateFileIdRows = await this.getRowsWhereColumnIsNotUniqueOrBlank(
name,
- "File ID"
+ PreDefinedColumn.FILE_ID
);
if (duplicateFileIdRows.length > 0) {
const rowNumbers = DatabaseService.truncateString(
@@ -246,21 +250,17 @@ export default abstract class DatabaseService {
100
);
errors.push(
- `"File ID" column contains duplicates. Found ${duplicateFileIdRows.length} duplicate values at rows ${rowNumbers}.`
+ `"${PreDefinedColumn.FILE_ID}" column contains duplicates. Found ${duplicateFileIdRows.length} duplicate ids at rows ${rowNumbers}.`
);
}
}
- if (!columnsOnTable.has("File Path")) {
- let error =
- '\
- "File Path" column is missing in the data source. \
- Check the data source header row for a "File Path" column name and try again.';
+ if (!columnsOnTable.has(PreDefinedColumn.FILE_PATH)) {
+ let error = `"${PreDefinedColumn.FILE_PATH}" column is missing in the data source.
+ Check the data source header row for a "${PreDefinedColumn.FILE_PATH}" column name and try again.`;
// Attempt to find a column with a similar name to "File Path"
const columns = Array.from(columnsOnTable);
- // TODO: In addition to doing this, on ingestion detect "file_path", "file path", etc.
- // and convert to "File Path" in DB table
const filePathLikeColumn =
columns.find((column) => column.toLowerCase().includes("path")) ||
columns.find((column) => column.toLowerCase().includes("file"));
@@ -274,14 +274,17 @@ export default abstract class DatabaseService {
errors.push(error);
} else {
// Check for empty or just whitespace File Path column values
- const blankFilePathRows = await this.getRowsWhereColumnIsBlank(name, "File Path");
+ const blankFilePathRows = await this.getRowsWhereColumnIsBlank(
+ name,
+ PreDefinedColumn.FILE_PATH
+ );
if (blankFilePathRows.length > 0) {
const rowNumbers = DatabaseService.truncateString(
blankFilePathRows.join(", "),
100
);
errors.push(
- `"File Path" column contains ${blankFilePathRows.length} empty or purely whitespace values at rows ${rowNumbers}.`
+ `"${PreDefinedColumn.FILE_PATH}" column contains ${blankFilePathRows.length} empty or purely whitespace paths at rows ${rowNumbers}.`
);
}
@@ -291,7 +294,7 @@ export default abstract class DatabaseService {
// Check for duplicate File ID column values
const duplicateFilePathRows = await this.getRowsWhereColumnIsNotUniqueOrBlank(
name,
- "File Path"
+ PreDefinedColumn.FILE_PATH
);
if (duplicateFilePathRows.length > 0) {
const rowNumbers = DatabaseService.truncateString(
@@ -299,7 +302,7 @@ export default abstract class DatabaseService {
100
);
errors.push(
- `"File Path" column contains duplicates, but has no "File ID" column to use as a unique identifier instead. Add a unique "File ID" column or make "File Path" values unique. Found ${duplicateFilePathRows.length} duplicate values at rows ${rowNumbers}.`
+ `"${PreDefinedColumn.FILE_PATH}" column contains duplicates, but has no "${PreDefinedColumn.FILE_ID}" column to use as a unique identifier instead. Add a unique "${PreDefinedColumn.FILE_ID}" column or make "${PreDefinedColumn.FILE_PATH}" values unique. Found ${duplicateFilePathRows.length} duplicate paths at rows ${rowNumbers}.`
);
}
}
From 0ed66faaf44a685c498970b56e5f292ff71e60c9 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Mon, 11 Nov 2024 22:20:28 -0800
Subject: [PATCH 04/15] Combine alter commands and add hidden UID
---
.../core/services/DatabaseService/index.ts | 92 +++++++++----------
1 file changed, 45 insertions(+), 47 deletions(-)
diff --git a/packages/core/services/DatabaseService/index.ts b/packages/core/services/DatabaseService/index.ts
index 133be927..f623eb21 100644
--- a/packages/core/services/DatabaseService/index.ts
+++ b/packages/core/services/DatabaseService/index.ts
@@ -23,6 +23,8 @@ const PRE_DEFINED_COLUMNS = Object.values(PreDefinedColumn);
export default abstract class DatabaseService {
protected readonly SOURCE_METADATA_TABLE = "source_metadata";
public static readonly LIST_DELIMITER = ",";
+ // Name of the hidden column BFF uses to uniquely identify rows
+ private static readonly HIDDEN_UID_COLUMN = "hidden_bff_uid";
// "Open file link" as a datatype must be hardcoded, and CAN NOT change
// without BREAKING visibility in the dataset released in 2024 as part
// of the EMT Data Release paper
@@ -155,7 +157,7 @@ export default abstract class DatabaseService {
);
}
- protected async deleteDataSource(dataSource: string): Promise {
+ private async deleteDataSource(dataSource: string): Promise {
this.existingDataSources.delete(dataSource);
this.dataSourceToAnnotationsMap.delete(dataSource);
await this.execute(`DROP TABLE IF EXISTS "${dataSource}"`);
@@ -166,38 +168,24 @@ export default abstract class DatabaseService {
MUST come after we check for errors so that we can rely on the table
to at least be valid before modifying it further
*/
- protected async addRequiredColumns(name: string): Promise {
- const dataSourceColumns = await this.getColumnsOnDataSource(name);
- if (!dataSourceColumns.has(PreDefinedColumn.FILE_ID)) {
- await this.execute(`
+ private async addRequiredColumns(name: string): Promise {
+ const commandsToExecute = [
+ // Add hidden UID column to uniquely identify rows
+ `
ALTER TABLE "${name}"
- ADD COLUMN IF NOT EXISTS "${PreDefinedColumn.FILE_ID}" VARCHAR;
- `);
- // The data source's input date gets appended to the end
- // of the auto-generated name, that makes it kind of messy
- // as an ID though so here lets remove it
- const dataSourceNameWithoutDate = name.split(" ")[0];
- // Auto-generates a File ID based on the row number when
- // ordered by file path.
- await this.execute(`
- UPDATE "${name}"
- SET "${PreDefinedColumn.FILE_ID}" = CONCAT(SQ.row, '-', '${dataSourceNameWithoutDate}')
- FROM (
- SELECT "${PreDefinedColumn.FILE_PATH}", ROW_NUMBER() OVER (ORDER BY "${PreDefinedColumn.FILE_PATH}") AS row
- FROM "${name}"
- ) AS SQ
- WHERE "${name}"."${PreDefinedColumn.FILE_PATH}" = SQ."${PreDefinedColumn.FILE_PATH}";
- `);
- }
+ ADD COLUMN IF NOT EXISTS "${DatabaseService.HIDDEN_UID_COLUMN}" INT NOT NULL PRIMARY KEY
+ `,
+ ];
+ const dataSourceColumns = await this.getColumnsOnDataSource(name);
if (!dataSourceColumns.has(PreDefinedColumn.FILE_NAME)) {
- await this.execute(`
+ commandsToExecute.push(`
ALTER TABLE "${name}"
ADD COLUMN IF NOT EXISTS "${PreDefinedColumn.FILE_NAME}" VARCHAR;
`);
// Best shot attempt at auto-generating a "File Name"
// from the "File Path", defaults to full path if this fails
- await this.execute(`
+ commandsToExecute.push(`
UPDATE "${name}"
SET "${PreDefinedColumn.FILE_NAME}" = COALESCE(
NULLIF(
@@ -211,6 +199,8 @@ export default abstract class DatabaseService {
`);
}
+ await this.execute(commandsToExecute.join("; "));
+
// Because we edited the column names this cache is now invalid
this.dataSourceToAnnotationsMap.delete(name);
}
@@ -220,7 +210,7 @@ export default abstract class DatabaseService {
the expectations around uniqueness/blankness for pre-defined columns
like "File Path", "File ID", etc.
*/
- protected async checkDataSourceForErrors(name: string): Promise {
+ private async checkDataSourceForErrors(name: string): Promise {
const errors: string[] = [];
const columnsOnTable = await this.getColumnsOnDataSource(name);
@@ -320,19 +310,21 @@ export default abstract class DatabaseService {
private async normalizeDataSourceColumnNames(dataSourceName: string): Promise {
const columnsOnDataSource = await this.getColumnsOnDataSource(dataSourceName);
- for (const preDefinedColumn of PRE_DEFINED_COLUMNS) {
+ const combinedAlterCommands = PRE_DEFINED_COLUMNS
// Filter out any pre-defined columns that are exact matches to columns on the data source
// since those are already perfect
- if (!columnsOnDataSource.has(preDefinedColumn)) {
+ .filter((preDefinedColumn) => !columnsOnDataSource.has(preDefinedColumn))
+ // Map the rest to SQL alter table commands to rename the columns
+ .flatMap((preDefinedColumn) => {
const preDefinedColumnSimplified = preDefinedColumn.toLowerCase().replace(" ", "");
// Grab near matches to the pre-defined columns like "file_name" for "File Name"
- const matches = [...columnsOnDataSource].filter((column) => {
- const simplifiedColumn = column
- .toLowerCase() // File Name -> file name
- .replaceAll(/\s|-|_/g, ""); // Matches any whitespace, hyphen, or underscore
- return simplifiedColumn === preDefinedColumnSimplified;
- });
+ const matches = [...columnsOnDataSource].filter(
+ (column) =>
+ preDefinedColumnSimplified ===
+ // Matches regardless of caps, whitespace, hyphens, or underscores
+ column.toLowerCase().replaceAll(/\s|-|_/g, "")
+ );
// Doesn't seem like we should guess at a pre-defined column match in this case
// so just toss up a user-actionable error to try to get them to retry
@@ -344,14 +336,22 @@ export default abstract class DatabaseService {
);
}
- if (matches.length === 1) {
- // Rename matching column name to new pre-defined column
- await this.execute(`
- ALTER TABLE "${dataSourceName}"
- RENAME COLUMN "${matches[0]}" TO '${preDefinedColumn}'
- `);
+ if (matches.length < 0) {
+ return []; // No-op essentially if no matches
}
- }
+
+ // Rename matching column name to new pre-defined column
+ return [
+ `
+ ALTER TABLE "${dataSourceName}"
+ RENAME COLUMN "${matches[0]}" TO '${preDefinedColumn}'
+ `,
+ ];
+ })
+ .join("; ");
+
+ if (combinedAlterCommands) {
+ await this.execute(combinedAlterCommands);
}
// Because we edited the column names this cache is now invalid
@@ -388,19 +388,16 @@ export default abstract class DatabaseService {
return duplicateColumnQueryResult.map((row) => row.row);
}
- // TODO: Triple check this is going to work still...
private async aggregateDataSources(dataSources: Source[]): Promise {
const viewName = dataSources
.map((source) => source.name)
.sort()
.join(", ");
- if (this.currentAggregateSource) {
+ if (this.currentAggregateSource === viewName) {
// Prevent adding the same data source multiple times by shortcutting out here
- if (this.currentAggregateSource === viewName) {
- return;
- }
-
+ return;
+ } else if (this.currentAggregateSource) {
// Otherwise, if an old aggregate exists, delete it
await this.deleteDataSource(this.currentAggregateSource);
}
@@ -461,6 +458,7 @@ export default abstract class DatabaseService {
.select("column_name, data_type")
.from('information_schema"."columns')
.where(`table_name = '${aggregateDataSourceName}'`)
+ .where(`column_name != '${DatabaseService.HIDDEN_UID_COLUMN}'`)
.toSQL();
const rows = await this.query(sql);
if (isEmpty(rows)) {
From 69841103b5fc865b1748e7ecc77b7c63a04ad02c Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Wed, 13 Nov 2024 09:39:44 -0800
Subject: [PATCH 05/15] Use UID instead of File iD
---
.../components/DataSourcePrompt/index.tsx | 10 +--
.../components/FileList/LazilyRenderedRow.tsx | 2 +-
.../FileList/LazilyRenderedThumbnail.tsx | 2 +-
packages/core/components/FileRow/index.tsx | 2 +-
packages/core/constants/index.ts | 7 +-
packages/core/entity/FileDetail/index.ts | 10 ++-
.../core/services/DatabaseService/index.ts | 74 +++++--------------
.../FileService/DatabaseFileService/index.ts | 59 +++++++--------
packages/core/state/interaction/logics.ts | 2 +-
9 files changed, 64 insertions(+), 104 deletions(-)
diff --git a/packages/core/components/DataSourcePrompt/index.tsx b/packages/core/components/DataSourcePrompt/index.tsx
index 0b004338..7e4af533 100644
--- a/packages/core/components/DataSourcePrompt/index.tsx
+++ b/packages/core/components/DataSourcePrompt/index.tsx
@@ -19,8 +19,8 @@ interface Props {
const ADDITIONAL_COLUMN_DETAILS = [
'If a "Thumbnail" column is present it should contain a web URL to a thumbnail image for the file. ',
'If a "File Name" column is present it should be the file\'s name (this will replace the "File Name" created by default from the path). ',
- 'If a "File Size" column is present it should contain the size of the file in bytes. ',
- 'If an "Uploaded" column is present it should contain the date the file was uploaded to the cloud storage and be formatted as YYYY-MM-DD HH:MM:SS.Z where Z is a timezone offset. ',
+ 'If a "File Size" column is present it should contain the size of the file in bytes. This is used for showing feedback to the user during downloads. ',
+ 'If an "Uploaded" column is present it should contain the date the file was uploaded to the storage and be formatted as YYYY-MM-DD HH:MM:SS.Z where Z is a timezone offset. ',
];
/**
@@ -81,10 +81,8 @@ export default function DataSourcePrompt(props: Props) {
<>
-
- The file must contain a "File Path" column & must be unique by
- the "File Path" column or have a unique "File ID"
- column. Any other columns are optional and will be available as
- annotations to query by.
+ The file must contain a "File Path" column. Any other columns
+ are optional and will be available as metadata to query by.
Advanced:
diff --git a/packages/core/components/FileList/LazilyRenderedRow.tsx b/packages/core/components/FileList/LazilyRenderedRow.tsx
index 81c21e8e..fccbb20a 100644
--- a/packages/core/components/FileList/LazilyRenderedRow.tsx
+++ b/packages/core/components/FileList/LazilyRenderedRow.tsx
@@ -67,7 +67,7 @@ export default function LazilyRenderedRow(props: LazilyRenderedRowProps) {
displayValue: annotation.extractFromFile(file),
width: columnWidths[annotation.name] || 1 / annotations.length,
}))}
- rowIdentifier={{ index, id: file.id }}
+ rowIdentifier={{ index, id: file.uid }}
onSelect={onSelect}
/>
);
diff --git a/packages/core/components/FileList/LazilyRenderedThumbnail.tsx b/packages/core/components/FileList/LazilyRenderedThumbnail.tsx
index 11506fd1..eca9f8c4 100644
--- a/packages/core/components/FileList/LazilyRenderedThumbnail.tsx
+++ b/packages/core/components/FileList/LazilyRenderedThumbnail.tsx
@@ -79,7 +79,7 @@ export default function LazilyRenderedThumbnail(props: LazilyRenderedThumbnailPr
if (onSelect && file !== undefined) {
onSelect(
- { index: overallIndex, id: file.id },
+ { index: overallIndex, id: file.uid },
{
// Details on different OS keybindings
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent#Properties
diff --git a/packages/core/components/FileRow/index.tsx b/packages/core/components/FileRow/index.tsx
index 75325cee..662d7054 100644
--- a/packages/core/components/FileRow/index.tsx
+++ b/packages/core/components/FileRow/index.tsx
@@ -19,7 +19,7 @@ export interface CellConfig {
interface FileRowProps {
cells: CellConfig[];
className?: string;
- rowIdentifier?: any;
+ rowIdentifier?: { index: number; id: string };
onContextMenu?: (evt: React.MouseEvent) => void;
onResize?: (columnKey: string, nextWidth?: number) => void;
onSelect?: OnSelect;
diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts
index 2c7275e4..6fbcd0bd 100644
--- a/packages/core/constants/index.ts
+++ b/packages/core/constants/index.ts
@@ -15,18 +15,17 @@ export const TOP_LEVEL_FILE_ANNOTATIONS = [
new Annotation({
annotationDisplayName: "File ID",
annotationName: AnnotationName.FILE_ID,
- description:
- "Auto or manually generated unique ID for file. Should not be used for collaboration or sharing as it may change.",
+ description: "ID for file.",
type: AnnotationType.STRING,
}),
new Annotation({
annotationDisplayName: "File Name",
annotationName: AnnotationName.FILE_NAME,
- description: "Name of file",
+ description: "Name of file.",
type: AnnotationType.STRING,
}),
new Annotation({
- annotationDisplayName: "File Path (Canonical)",
+ annotationDisplayName: "File Path",
annotationName: AnnotationName.FILE_PATH,
description: "Path to file in storage.",
type: AnnotationType.STRING,
diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts
index 4d8258d3..83ddc9f4 100644
--- a/packages/core/entity/FileDetail/index.ts
+++ b/packages/core/entity/FileDetail/index.ts
@@ -73,7 +73,8 @@ export interface FmsFile {
* Facade for a FileDetailResponse.
*/
export default class FileDetail {
- private fileDetail: FmsFile;
+ private readonly fileDetail: FmsFile;
+ private readonly uniqueId?: string;
private static convertAicsDrivePathToAicsS3Path(path: string): string {
const pathWithoutDrive = path.replace("/allen/programs/allencell/data/proj0", "");
@@ -82,14 +83,19 @@ export default class FileDetail {
return `https://s3.us-west-2.amazonaws.com/production.files.allencell.org${pathWithoutDrive}`;
}
- constructor(fileDetail: FmsFile) {
+ constructor(fileDetail: FmsFile, uniqueId?: string) {
this.fileDetail = fileDetail;
+ this.uniqueId = uniqueId;
}
public get details() {
return this.fileDetail;
}
+ public get uid(): string {
+ return this.uniqueId || this.id;
+ }
+
public get id(): string {
const id = this.fileDetail.file_id || this.getFirstAnnotationValue("File ID");
if (id === undefined) {
diff --git a/packages/core/services/DatabaseService/index.ts b/packages/core/services/DatabaseService/index.ts
index f623eb21..382ae974 100644
--- a/packages/core/services/DatabaseService/index.ts
+++ b/packages/core/services/DatabaseService/index.ts
@@ -21,10 +21,10 @@ const PRE_DEFINED_COLUMNS = Object.values(PreDefinedColumn);
* Service reponsible for querying against a database
*/
export default abstract class DatabaseService {
- protected readonly SOURCE_METADATA_TABLE = "source_metadata";
public static readonly LIST_DELIMITER = ",";
+ public static readonly HIDDEN_UID_ANNOTATION = "hidden_bff_uid";
// Name of the hidden column BFF uses to uniquely identify rows
- private static readonly HIDDEN_UID_COLUMN = "hidden_bff_uid";
+ protected readonly SOURCE_METADATA_TABLE = "source_metadata";
// "Open file link" as a datatype must be hardcoded, and CAN NOT change
// without BREAKING visibility in the dataset released in 2024 as part
// of the EMT Data Release paper
@@ -173,7 +173,19 @@ export default abstract class DatabaseService {
// Add hidden UID column to uniquely identify rows
`
ALTER TABLE "${name}"
- ADD COLUMN IF NOT EXISTS "${DatabaseService.HIDDEN_UID_COLUMN}" INT NOT NULL PRIMARY KEY
+ ADD COLUMN ${DatabaseService.HIDDEN_UID_ANNOTATION} INT
+ `,
+ // Altering tables to add primary keys or serially generated columns
+ // isn't yet supported in DuckDB, so this does a serially generated
+ // column addition manually
+ `
+ UPDATE "${name}"
+ SET "${DatabaseService.HIDDEN_UID_ANNOTATION}" = SQ.row
+ FROM (
+ SELECT "${PreDefinedColumn.FILE_PATH}", ROW_NUMBER() OVER (ORDER BY "${PreDefinedColumn.FILE_PATH}") AS row
+ FROM "${name}"
+ ) AS SQ
+ WHERE "${name}"."${PreDefinedColumn.FILE_PATH}" = SQ."${PreDefinedColumn.FILE_PATH}";
`,
];
@@ -181,7 +193,7 @@ export default abstract class DatabaseService {
if (!dataSourceColumns.has(PreDefinedColumn.FILE_NAME)) {
commandsToExecute.push(`
ALTER TABLE "${name}"
- ADD COLUMN IF NOT EXISTS "${PreDefinedColumn.FILE_NAME}" VARCHAR;
+ ADD COLUMN "${PreDefinedColumn.FILE_NAME}" VARCHAR;
`);
// Best shot attempt at auto-generating a "File Name"
// from the "File Path", defaults to full path if this fails
@@ -214,37 +226,6 @@ export default abstract class DatabaseService {
const errors: string[] = [];
const columnsOnTable = await this.getColumnsOnDataSource(name);
- // If a data source has a File ID it must also pass validation
- const hasFileIdColumn = columnsOnTable.has(PreDefinedColumn.FILE_ID);
- if (hasFileIdColumn) {
- // Check for empty or just whitespace File ID column values
- const blankFileIdRows = await this.getRowsWhereColumnIsBlank(
- name,
- PreDefinedColumn.FILE_ID
- );
- if (blankFileIdRows.length > 0) {
- const rowNumbers = DatabaseService.truncateString(blankFileIdRows.join(", "), 100);
- errors.push(
- `"${PreDefinedColumn.FILE_ID}" column contains ${blankFileIdRows.length} empty or purely whitespace ids at rows ${rowNumbers}.`
- );
- }
-
- // Check for duplicate File ID column values
- const duplicateFileIdRows = await this.getRowsWhereColumnIsNotUniqueOrBlank(
- name,
- PreDefinedColumn.FILE_ID
- );
- if (duplicateFileIdRows.length > 0) {
- const rowNumbers = DatabaseService.truncateString(
- duplicateFileIdRows.join(", "),
- 100
- );
- errors.push(
- `"${PreDefinedColumn.FILE_ID}" column contains duplicates. Found ${duplicateFileIdRows.length} duplicate ids at rows ${rowNumbers}.`
- );
- }
- }
-
if (!columnsOnTable.has(PreDefinedColumn.FILE_PATH)) {
let error = `"${PreDefinedColumn.FILE_PATH}" column is missing in the data source.
Check the data source header row for a "${PreDefinedColumn.FILE_PATH}" column name and try again.`;
@@ -277,25 +258,6 @@ export default abstract class DatabaseService {
`"${PreDefinedColumn.FILE_PATH}" column contains ${blankFilePathRows.length} empty or purely whitespace paths at rows ${rowNumbers}.`
);
}
-
- // "File Path" has to be unique when a unique File ID is not provided
- // otherwise we can't cleanly auto-generate a File ID based on the File Path
- if (!hasFileIdColumn) {
- // Check for duplicate File ID column values
- const duplicateFilePathRows = await this.getRowsWhereColumnIsNotUniqueOrBlank(
- name,
- PreDefinedColumn.FILE_PATH
- );
- if (duplicateFilePathRows.length > 0) {
- const rowNumbers = DatabaseService.truncateString(
- duplicateFilePathRows.join(", "),
- 100
- );
- errors.push(
- `"${PreDefinedColumn.FILE_PATH}" column contains duplicates, but has no "${PreDefinedColumn.FILE_ID}" column to use as a unique identifier instead. Add a unique "${PreDefinedColumn.FILE_ID}" column or make "${PreDefinedColumn.FILE_PATH}" values unique. Found ${duplicateFilePathRows.length} duplicate paths at rows ${rowNumbers}.`
- );
- }
- }
}
return errors;
@@ -336,7 +298,7 @@ export default abstract class DatabaseService {
);
}
- if (matches.length < 0) {
+ if (matches.length < 1) {
return []; // No-op essentially if no matches
}
@@ -458,7 +420,7 @@ export default abstract class DatabaseService {
.select("column_name, data_type")
.from('information_schema"."columns')
.where(`table_name = '${aggregateDataSourceName}'`)
- .where(`column_name != '${DatabaseService.HIDDEN_UID_COLUMN}'`)
+ .where(`column_name != '${DatabaseService.HIDDEN_UID_ANNOTATION}'`)
.toSQL();
const rows = await this.query(sql);
if (isEmpty(rows)) {
diff --git a/packages/core/services/FileService/DatabaseFileService/index.ts b/packages/core/services/FileService/DatabaseFileService/index.ts
index dca2b9ed..4ec842bc 100644
--- a/packages/core/services/FileService/DatabaseFileService/index.ts
+++ b/packages/core/services/FileService/DatabaseFileService/index.ts
@@ -1,4 +1,4 @@
-import { isEmpty, isNil, omit, uniqueId } from "lodash";
+import { isEmpty, isNil, uniqueId } from "lodash";
import FileService, { GetFilesRequest, SelectionAggregationResult, Selection } from "..";
import DatabaseService from "../../DatabaseService";
@@ -25,36 +25,29 @@ export default class DatabaseFileService implements FileService {
private readonly dataSourceNames: string[];
private static convertDatabaseRowToFileDetail(row: { [key: string]: string }): FileDetail {
- const annotations = [
- { name: "File Path", values: [row["File Path"]] },
- { name: "File ID", values: [row["File ID"]] },
- { name: "File Name", values: [row["File Name"]] },
- ];
- if (!isNil(row["File Size"])) {
- annotations.push({ name: "File Size", values: [row["File Size"]] });
+ const uniqueId: string | undefined = row[DatabaseService.HIDDEN_UID_ANNOTATION];
+ if (!uniqueId) {
+ throw new Error("Missing auto-generated unique ID");
}
- if (row["Thumbnail"]) {
- annotations.push({ name: "Thumbnail", values: [row["Thumbnail"]] });
- }
- if (row["Uploaded"]) {
- annotations.push({ name: "Uploaded", values: [row["Uploaded"]] });
- }
- return new FileDetail({
- annotations: [
- ...Object.entries(omit(row, ...annotations.keys())).flatMap(([name, values]: any) =>
- values !== null
- ? [
- {
- name,
- values: `${values}`
- .split(DatabaseService.LIST_DELIMITER)
- .map((value: string) => value.trim()),
- },
- ]
- : []
- ),
- ],
- });
+
+ return new FileDetail(
+ {
+ annotations: Object.entries(row)
+ .filter(
+ ([name, values]) =>
+ !isNil(values) &&
+ // Omit hidden UID annotation
+ name !== DatabaseService.HIDDEN_UID_ANNOTATION
+ )
+ .map(([name, values]) => ({
+ name,
+ values: `${values}`
+ .split(DatabaseService.LIST_DELIMITER)
+ .map((value: string) => value.trim()),
+ })),
+ },
+ uniqueId
+ );
}
constructor(
@@ -134,7 +127,7 @@ export default class DatabaseFileService implements FileService {
selections.forEach((selection) => {
selection.indexRanges.forEach((indexRange) => {
const subQuery = new SQLBuilder()
- .select('"File Path"')
+ .select(`${DatabaseService.HIDDEN_UID_ANNOTATION}`)
.from(this.dataSourceNames)
.offset(indexRange.start)
.limit(indexRange.end - indexRange.start + 1);
@@ -156,7 +149,9 @@ export default class DatabaseFileService implements FileService {
);
}
- sqlBuilder.whereOr(`"File Path" IN (${subQuery.toSQL()})`);
+ sqlBuilder.whereOr(
+ `${DatabaseService.HIDDEN_UID_ANNOTATION} IN (${subQuery.toSQL()})`
+ );
});
});
diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts
index 9c7eb4eb..94ef5e22 100644
--- a/packages/core/state/interaction/logics.ts
+++ b/packages/core/state/interaction/logics.ts
@@ -213,7 +213,7 @@ const downloadFilesLogic = createLogic({
} else {
const selectedFilesDetails = await fileSelection.fetchAllDetails();
filesToDownload = selectedFilesDetails.map((file) => ({
- id: file.id,
+ id: file.uid,
name: file.name,
size: file.size,
path: file.downloadPath,
From bcb08dce24e0310a16d161e375fcd46276323ec1 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Wed, 13 Nov 2024 09:43:27 -0800
Subject: [PATCH 06/15] Use uid in more places
---
packages/core/components/FileDetails/index.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/core/components/FileDetails/index.tsx b/packages/core/components/FileDetails/index.tsx
index a7b02e21..fb43ce5a 100644
--- a/packages/core/components/FileDetails/index.tsx
+++ b/packages/core/components/FileDetails/index.tsx
@@ -99,7 +99,7 @@ export default function FileDetails(props: Props) {
dispatch(
interaction.actions.downloadFiles([
{
- id: fileDetails.id,
+ id: fileDetails.uid,
name: fileDetails.name,
size: fileDetails.size,
path: fileDetails.downloadPath,
@@ -145,7 +145,7 @@ export default function FileDetails(props: Props) {
- status.data.fileId?.includes(fileDetails.id)
+ status.data.fileId?.includes(fileDetails.uid)
)}
iconName="Download"
text="Download"
From b7a1438d7030a1ee5a2ec8349bd004decfbbd356 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Wed, 13 Nov 2024 09:55:57 -0800
Subject: [PATCH 07/15] Use hidden id in tests
---
.../DatabaseFileService/test/DatabaseFileService.test.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts b/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts
index 27adaa2a..03b32f85 100644
--- a/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts
+++ b/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts
@@ -1,5 +1,6 @@
import { expect } from "chai";
+import DatabaseService from "../../../DatabaseService";
import FileSelection from "../../../../entity/FileSelection";
import FileSet from "../../../../entity/FileSet";
import NumericRange from "../../../../entity/NumericRange";
@@ -12,7 +13,7 @@ describe("DatabaseFileService", () => {
const totalFileSize = 864452;
const fileIds = ["abc123", "def456"];
const files = fileIds.map((file_id) => ({
- "File ID": file_id,
+ [DatabaseService.HIDDEN_UID_ANNOTATION]: file_id,
"File Size": `${totalFileSize / 2}`,
"File Path": "path/to/file",
"File Name": "file",
From 09858bd1f544232bce1be31583ebf50e2bb7a3c8 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Wed, 13 Nov 2024 09:58:39 -0800
Subject: [PATCH 08/15] Add canonical path back
---
packages/core/constants/index.ts | 2 +-
.../DatabaseFileService/test/DatabaseFileService.test.ts | 4 ----
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts
index 6fbcd0bd..283b0b11 100644
--- a/packages/core/constants/index.ts
+++ b/packages/core/constants/index.ts
@@ -25,7 +25,7 @@ export const TOP_LEVEL_FILE_ANNOTATIONS = [
type: AnnotationType.STRING,
}),
new Annotation({
- annotationDisplayName: "File Path",
+ annotationDisplayName: "File Path (Canonical)",
annotationName: AnnotationName.FILE_PATH,
description: "Path to file in storage.",
type: AnnotationType.STRING,
diff --git a/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts b/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts
index 03b32f85..3269add3 100644
--- a/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts
+++ b/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts
@@ -44,10 +44,6 @@ describe("DatabaseFileService", () => {
expect(data.length).to.equal(2);
expect(data[0].details).to.deep.equal({
annotations: [
- {
- name: "File ID",
- values: ["abc123"],
- },
{
name: "File Size",
values: ["432226"],
From 00af6b800f08bd962fd0ee5eed2d86bcf3f69db4 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Wed, 13 Nov 2024 11:03:45 -0800
Subject: [PATCH 09/15] Improve data source prompt explanation
---
.../DataSourcePrompt.module.css | 9 +++++
.../components/DataSourcePrompt/index.tsx | 40 ++++++++++++++++---
2 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css b/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css
index 74c8ec2f..78e3b530 100644
--- a/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css
+++ b/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css
@@ -53,6 +53,15 @@
justify-content: left;
}
+.table-example, .table-example > tr > :is(th, td) {
+ border: 2px solid var(--secondary-background-color);
+ border-collapse: collapse;
+}
+
+.table-example > tr > :is(th, td) {
+ padding: 5px;
+}
+
.title {
margin-bottom: 6px;
}
diff --git a/packages/core/components/DataSourcePrompt/index.tsx b/packages/core/components/DataSourcePrompt/index.tsx
index 7e4af533..453f6d0d 100644
--- a/packages/core/components/DataSourcePrompt/index.tsx
+++ b/packages/core/components/DataSourcePrompt/index.tsx
@@ -77,14 +77,42 @@ export default function DataSourcePrompt(props: Props) {
To get started, load a CSV, Parquet, or JSON file containing metadata (annotations)
about your files to view them.
+
+ The first row should be the metadata tags and each row proceeding it should be
+ metadata about a file. The only required column is "File Path" which is
+ intended to represent the path to a file in storage. Any other columns are optional
+ and will be available as metadata to query by.
+
+
+ Example CSV:
+
+
+
+ File Path |
+ Gene (Just an example) |
+ Color (Also an example) |
+
+
+ /some/path/to/storage/somewhere.zarr |
+ CDH2 |
+ Blue |
+
+
+ /another/path/to/another/file.txt |
+ VIM |
+ Green |
+
+
{isDataSourceDetailExpanded ? (
<>
-
- -
- The file must contain a "File Path" column. Any other columns
- are optional and will be available as metadata to query by.
-
-
Advanced:
-
From 2888c046854cf5d659b09f74158ad3fcc3ed7f19 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Wed, 13 Nov 2024 11:27:51 -0800
Subject: [PATCH 10/15] Upgrade styling of csv example
---
.../DataSourcePrompt/DataSourcePrompt.module.css | 11 ++++++++++-
.../core/components/DataSourcePrompt/index.tsx | 14 +++++++++-----
2 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css b/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css
index 78e3b530..776c4767 100644
--- a/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css
+++ b/packages/core/components/DataSourcePrompt/DataSourcePrompt.module.css
@@ -62,12 +62,21 @@
padding: 5px;
}
+.light-border.table-example, .light-border.table-example > tr > :is(th, td) {
+ border: 2px solid var(--primary-background-color);
+}
+
+.left-text-align {
+ text-align: left !important;
+}
+
.title {
margin-bottom: 6px;
}
.datasource-subhead {
- margin-bottom: 26px !important;
+ margin-bottom: 13px !important;
+ text-align: left;
font-weight: 600;
}
diff --git a/packages/core/components/DataSourcePrompt/index.tsx b/packages/core/components/DataSourcePrompt/index.tsx
index 453f6d0d..b3190e9f 100644
--- a/packages/core/components/DataSourcePrompt/index.tsx
+++ b/packages/core/components/DataSourcePrompt/index.tsx
@@ -82,19 +82,23 @@ export default function DataSourcePrompt(props: Props) {
[styles.datasourceSubhead]: !props?.hideTitle,
})}
>
- The first row should be the metadata tags and each row proceeding it should be
- metadata about a file. The only required column is "File Path" which is
- intended to represent the path to a file in storage. Any other columns are optional
- and will be available as metadata to query by.
+ The first row should contain metadata tags, and each subsequent row includes
+ metadata for a file, with "File Path" being the only required column.
+ Other columns are optional and can be used for querying additional file metadata.
Example CSV:
-
+
File Path |
Gene (Just an example) |
From 8f6e19b18cb05602bca9a2600783ea2fe22a577c Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Wed, 13 Nov 2024 12:11:19 -0800
Subject: [PATCH 11/15] Add tbody/thead
---
.../components/DataSourcePrompt/index.tsx | 34 +++++++++++--------
1 file changed, 19 insertions(+), 15 deletions(-)
diff --git a/packages/core/components/DataSourcePrompt/index.tsx b/packages/core/components/DataSourcePrompt/index.tsx
index b3190e9f..b4a55e1f 100644
--- a/packages/core/components/DataSourcePrompt/index.tsx
+++ b/packages/core/components/DataSourcePrompt/index.tsx
@@ -99,21 +99,25 @@ export default function DataSourcePrompt(props: Props) {
[styles.lightBorder]: !props?.hideTitle,
})}
>
-
- File Path |
- Gene (Just an example) |
- Color (Also an example) |
-
-
- /some/path/to/storage/somewhere.zarr |
- CDH2 |
- Blue |
-
-
- /another/path/to/another/file.txt |
- VIM |
- Green |
-
+
+
+ File Path |
+ Gene (Just an example) |
+ Color (Also an example) |
+
+
+
+
+ /some/path/to/storage/somewhere.zarr |
+ CDH2 |
+ Blue |
+
+
+ /another/path/to/another/file.txt |
+ VIM |
+ Green |
+
+
{isDataSourceDetailExpanded ? (
<>
From 99d172e35b568cbab19eb35777fcd4f302c219e3 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Thu, 14 Nov 2024 12:48:41 -0800
Subject: [PATCH 12/15] Move comment
---
packages/core/services/DatabaseService/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/services/DatabaseService/index.ts b/packages/core/services/DatabaseService/index.ts
index 382ae974..c3f31788 100644
--- a/packages/core/services/DatabaseService/index.ts
+++ b/packages/core/services/DatabaseService/index.ts
@@ -22,8 +22,8 @@ const PRE_DEFINED_COLUMNS = Object.values(PreDefinedColumn);
*/
export default abstract class DatabaseService {
public static readonly LIST_DELIMITER = ",";
- public static readonly HIDDEN_UID_ANNOTATION = "hidden_bff_uid";
// Name of the hidden column BFF uses to uniquely identify rows
+ public static readonly HIDDEN_UID_ANNOTATION = "hidden_bff_uid";
protected readonly SOURCE_METADATA_TABLE = "source_metadata";
// "Open file link" as a datatype must be hardcoded, and CAN NOT change
// without BREAKING visibility in the dataset released in 2024 as part
From c901b9a7823abf7289e17afd8739e2bdf0ba2e12 Mon Sep 17 00:00:00 2001
From: SeanLeRoy
Date: Thu, 14 Nov 2024 12:55:58 -0800
Subject: [PATCH 13/15] Typo
---
packages/core/components/DataSourcePrompt/index.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/core/components/DataSourcePrompt/index.tsx b/packages/core/components/DataSourcePrompt/index.tsx
index b4a55e1f..cfde006a 100644
--- a/packages/core/components/DataSourcePrompt/index.tsx
+++ b/packages/core/components/DataSourcePrompt/index.tsx
@@ -82,9 +82,9 @@ export default function DataSourcePrompt(props: Props) {
[styles.datasourceSubhead]: !props?.hideTitle,
})}
>
- The first row should contain metadata tags, and each subsequent row includes
- metadata for a file, with "File Path" being the only required column.
- Other columns are optional and can be used for querying additional file metadata.
+ The first row should contain metadata tags, and each subsequent row include metadata
+ for a file, with "File Path" being the only required column. Other columns
+ are optional and can be used for querying additional file metadata.
Date: Mon, 18 Nov 2024 15:05:48 -0800
Subject: [PATCH 14/15] prevent button from refocusing on menu close
---
packages/core/components/Buttons/BaseButton.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/core/components/Buttons/BaseButton.tsx b/packages/core/components/Buttons/BaseButton.tsx
index 5dd608af..c5424cf1 100644
--- a/packages/core/components/Buttons/BaseButton.tsx
+++ b/packages/core/components/Buttons/BaseButton.tsx
@@ -1,5 +1,6 @@
import { DefaultButton, DirectionalHint, IContextualMenuItem, Icon } from "@fluentui/react";
import classNames from "classnames";
+import { noop } from "lodash";
import * as React from "react";
import useButtonMenu from "./useButtonMenu";
@@ -28,6 +29,7 @@ export default function BaseButton(props: Props) {
const styledMenu = useButtonMenu({
items: props.menuItems,
directionalHint: props.menuDirection,
+ onRestoreFocus: noop, // Prevent button from refocusing on menu close
});
const content = (
From d7e9e998ad4c6dae20b36d91338e4e0082a012fe Mon Sep 17 00:00:00 2001
From: Anya Wallace
Date: Mon, 18 Nov 2024 16:23:41 -0800
Subject: [PATCH 15/15] move refocus change to apply to all menu buttons
---
packages/core/components/Buttons/BaseButton.tsx | 2 --
packages/core/components/Buttons/useButtonMenu.ts | 2 ++
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/core/components/Buttons/BaseButton.tsx b/packages/core/components/Buttons/BaseButton.tsx
index c5424cf1..5dd608af 100644
--- a/packages/core/components/Buttons/BaseButton.tsx
+++ b/packages/core/components/Buttons/BaseButton.tsx
@@ -1,6 +1,5 @@
import { DefaultButton, DirectionalHint, IContextualMenuItem, Icon } from "@fluentui/react";
import classNames from "classnames";
-import { noop } from "lodash";
import * as React from "react";
import useButtonMenu from "./useButtonMenu";
@@ -29,7 +28,6 @@ export default function BaseButton(props: Props) {
const styledMenu = useButtonMenu({
items: props.menuItems,
directionalHint: props.menuDirection,
- onRestoreFocus: noop, // Prevent button from refocusing on menu close
});
const content = (
diff --git a/packages/core/components/Buttons/useButtonMenu.ts b/packages/core/components/Buttons/useButtonMenu.ts
index 4164c3e7..42860cbb 100644
--- a/packages/core/components/Buttons/useButtonMenu.ts
+++ b/packages/core/components/Buttons/useButtonMenu.ts
@@ -1,4 +1,5 @@
import { ContextualMenuItemType, IContextualMenuItem, IContextualMenuProps } from "@fluentui/react";
+import { noop } from "lodash";
import * as React from "react";
import styles from "./useButtonMenu.module.css";
@@ -30,6 +31,7 @@ function normalizeButtonMenu(menu: IContextualMenuProps): IContextualMenuProps {
className: styles.buttonMenu,
calloutProps: { className: styles.buttonMenuCallout },
items: menu.items.map((item) => normalizeButtonMenuItem(item)),
+ onRestoreFocus: noop, // Prevent button from refocusing on menu close
};
}