Skip to content

Commit

Permalink
✨ QueryBuilder CustomPill
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviercperrier committed Mar 3, 2025
1 parent 9aa0927 commit 055a20b
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 74 deletions.
20 changes: 19 additions & 1 deletion frontend/components/base/dialog/alert-dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
AlertDialogTitle,
} from "../ui/alert-dialog";
import { alertDialog, OpenAlertDialogProps } from "./alert-dialog-store";
import { Spinner } from "../spinner";

type AlertDialogProps = OpenAlertDialogProps & {
isOpen: boolean;
Expand All @@ -33,6 +34,7 @@ const AlertDialogContext = createContext<AlertDialogContextType | undefined>(
);

export const AlertDialogProvider = ({ children }: { children: ReactNode }) => {
const [loading, setLoading] = useState(false);
const [activeAlertDialog, setActiveAlertDialog] =
useState<AlertDialogProps | null>();

Expand Down Expand Up @@ -92,7 +94,23 @@ export const AlertDialogProvider = ({ children }: { children: ReactNode }) => {
{activeAlertDialog.hideCancel ? null : (
<AlertDialogCancel {...activeAlertDialog.cancelProps} />
)}
<AlertDialogAction {...activeAlertDialog.actionProps} />
<AlertDialogAction
{...activeAlertDialog.actionProps}
disabled={activeAlertDialog.actionProps.disabled || loading}
children={
<>
{loading && <Spinner />}
{activeAlertDialog.actionProps.children}
</>
}
onClick={async (e) => {
e.preventDefault();
setLoading(true);
await activeAlertDialog?.actionProps?.onClick?.(e);
setLoading(false);
close();
}}
/>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Expand Down
5 changes: 3 additions & 2 deletions frontend/components/base/dialog/alert-dialog-store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ReactNode } from "react";
import {
AlertDialogActionProps,
AlertDialogCancelProps,
Expand All @@ -7,7 +8,7 @@ export type AlertDialogType = "success" | "info" | "warning" | "error";

interface OpenAlertDialogBaseProps {
title: string;
description: string;
description: ReactNode;
type?: AlertDialogType;
actionProps: AlertDialogActionProps;
className?: string;
Expand All @@ -19,7 +20,7 @@ interface OpenAlertDialogWithCancelProps extends OpenAlertDialogBaseProps {
}

interface OpenAlertDialogWithoutCancelProps extends OpenAlertDialogBaseProps {
hideCancel: true;
hideCancel?: true;
cancelProps?: never;
}

Expand Down
55 changes: 0 additions & 55 deletions frontend/components/feature/query-builder/alerts.ts

This file was deleted.

112 changes: 112 additions & 0 deletions frontend/components/feature/query-builder/alerts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { alertDialog } from "@/components/base/dialog/alert-dialog-store";
import { QueryBuilderDictionary } from "./types";
import {
QueryBuilderInstance,
SavedFilterInstance,
} from "@/components/model/query-builder-core";
import { ISavedFilter } from "@/components/model/saved-filter";

export function openDeleteSavedFilterAlert(
savedFilter: SavedFilterInstance,
dict: QueryBuilderDictionary
) {
alertDialog.open({
type: "warning",
title: dict.savedFilter.deleteDialog.title,
description: dict.savedFilter.deleteDialog.description,
cancelProps: {
children: dict.savedFilter.deleteDialog.cancel,
},
actionProps: {
variant: "destructive",
onClick: () => savedFilter.delete(),
children: dict.savedFilter.deleteDialog.ok,
},
});
}

export function openOverwriteSavedFilterAlert(
queryBuilder: QueryBuilderInstance,
dict: QueryBuilderDictionary
) {
alertDialog.open({
type: "warning",
title: dict.savedFilter.overwriteDialog.title,
description: dict.savedFilter.overwriteDialog.description,
cancelProps: {
children: dict.savedFilter.overwriteDialog.cancel,
},
actionProps: {
onClick: () => queryBuilder.createSavedFilter(),
children: dict.savedFilter.overwriteDialog.ok,
},
});
}

export function openCustomPillCantBeEmptyDialog(dict: QueryBuilderDictionary) {
alertDialog.open({
type: "error",
title: dict.queryPill.customPill.cantBeEmptyDialog.title,
description: dict.queryPill.customPill.cantBeEmptyDialog.description,
hideCancel: true,
actionProps: {
children: dict.queryPill.customPill.cantBeEmptyDialog.ok,
},
});
}

export function openCustomPillTitleExistsDialog(dict: QueryBuilderDictionary) {
alertDialog.open({
type: "error",
title: dict.queryPill.customPill.titleExistsDialog.title,
description: dict.queryPill.customPill.titleExistsDialog.description,
hideCancel: true,
actionProps: {
children: dict.queryPill.customPill.titleExistsDialog.ok,
},
});
}

export function openCustomPillSaveDialog(
dict: QueryBuilderDictionary,
title: string,
associatedSavedFilters: ISavedFilter[] | undefined,
onSave: () => Promise<void>
) {
alertDialog.open({
type: "warning",
title: dict.queryPill.customPill.saveDialog.title,
description: (
<div className="space-y-4">
<div>
{dict.queryPill.customPill.saveDialog.confirmationMessage.replace(
"{title}",
title
)}
</div>
{associatedSavedFilters?.length && (
<div className="border rounded p-4 space-y-2">
<div className="font-medium text-black">
{dict.queryPill.customPill.saveDialog.affectedFilters}
</div>
<ul className="list-disc list-inside">
{associatedSavedFilters.map((filter) => (
<li className="list-item">{filter.title}</li>
))}
</ul>
</div>
)}
</div>
),
cancelProps: {
children: dict.queryPill.customPill.saveDialog.cancel,
},
actionProps: {
children: dict.queryPill.customPill.saveDialog.ok,
onClick: async (e) => {
e.preventDefault();
return onSave();
},
},
});
}
16 changes: 15 additions & 1 deletion frontend/components/feature/query-builder/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,25 @@ export const defaultDictionary: QueryBuilderDictionary = {
cancel: "Cancel",
ok: "Save",
},
cantBeEmptyDialod: {
cantBeEmptyDialog: {
title: "Query cannot be empty",
description: "Your custom query must contain at least one criteria.",
ok: "Close",
},
titleExistsDialog: {
title: "Name already in use",
description:
"A custom query with this name already exists. Please assign a unique name.",
ok: "Close",
},
saveDialog: {
title: "Edit this query?",
confirmationMessage:
'You are about to edit the custom query "{title}", which may affect your results.',
affectedFilters: "Affected saved filters:",
cancel: "Cancel",
ok: "Save",
},
},
},
toolbar: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import {
useQueryBuilder,
} from "@/components/model/query-builder-core";
import { QueryBarContext } from "../query-bar/query-bar-context";
import { openCustomPillCantBeEmptyDialog } from "../alerts";
import {
openCustomPillCantBeEmptyDialog,
openCustomPillSaveDialog,
openCustomPillTitleExistsDialog,
} from "../alerts";

function QueryPillCustomEditDialog({
open,
Expand Down Expand Up @@ -70,7 +74,9 @@ function QueryPillCustomEditDialog({

const coreQuery = customQueryBuilder.getQueries()[0];
const coreSavedFilter = customQueryBuilder.getSelectedSavedFilter();
const hasChanged = title !== queryPill.title || coreSavedFilter?.isDirty();

const titleChanged = title !== queryPill.title;
const hasChanged = titleChanged || coreSavedFilter?.isDirty();

const handleOnOpenChange = useCallback(
function (open: boolean) {
Expand All @@ -85,21 +91,31 @@ function QueryPillCustomEditDialog({
);

const handleSave = useCallback(
function () {
// TODO: Before updating check:
// 1. If the query is empty, show an alert
// 2. If title is updated, validate the title
// 3. Fetch filtersByPill, show alert and optionaly show affected filters

async function () {
if (coreQuery.isEmpty()) {
openCustomPillCantBeEmptyDialog(dict);
} else {
return;
}

if (titleChanged) {
const valid = await customPillConfig?.validateCustomPillTitle(title);

if (valid !== undefined && valid === false) {
openCustomPillTitleExistsDialog(dict);
return;
}
}

const associatedSavedFilters =
await customPillConfig?.fetchSavedFiltersByCustomPillId(queryPill.id);

openCustomPillSaveDialog(dict, title, associatedSavedFilters, async () =>
coreSavedFilter?.save(SavedFilterTypeEnum.Query, {
title,
});
}
})
);
},
[coreQuery, coreSavedFilter, dict, title]
[coreQuery, coreSavedFilter, dict, title, queryPill.id, customPillConfig]
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function SavedFiltersSelect() {
disabled={savedFilters.length === 0}
>
<Tooltip open={savedFilters.length > 0 ? false : undefined}>
<TooltipTrigger>
<TooltipTrigger asChild>
<SelectTrigger className="w-[135px] h-7">
<div className="flex items-center gap-2">
<FolderIcon size={14} /> {dict.savedFilter.myFilters}
Expand Down
16 changes: 14 additions & 2 deletions frontend/components/feature/query-builder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type QueryPillCustomConfig = {
/**
* Validate the title of the custom pill
*/
validateCustomPillTitle: (title: string, tag: string) => Promise<boolean>;
validateCustomPillTitle: (title: string) => Promise<boolean>;
/**
* Get the custom pill by id
*/
Expand Down Expand Up @@ -139,11 +139,23 @@ export type QueryBuilderDictionary = {
cancel: string;
ok: string;
};
cantBeEmptyDialod: {
cantBeEmptyDialog: {
title: string;
description: string;
ok: string;
};
titleExistsDialog: {
title: string;
description: string;
ok: string;
};
saveDialog: {
title: string;
confirmationMessage: `${string}{title}${string}`;
affectedFilters: string;
cancel: string;
ok: string;
};
};
};
toolbar: {
Expand Down

0 comments on commit 055a20b

Please sign in to comment.