@canva/cli 0.0.1-beta.30 → 0.0.1-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canva/cli",
3
- "version": "0.0.1-beta.30",
3
+ "version": "0.0.1-beta.32",
4
4
  "description": "The official Canva CLI.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "Canva Pty Ltd.",
@@ -6,11 +6,11 @@
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
7
  "author": "Canva Pty Ltd.",
8
8
  "dependencies": {
9
- "@canva/app-ui-kit": "^4.9.0",
9
+ "@canva/app-ui-kit": "^4.10.0",
10
10
  "@canva/asset": "^2.2.0",
11
- "@canva/design": "^2.4.1",
11
+ "@canva/design": "^2.6.0",
12
12
  "@canva/error": "^2.1.0",
13
- "@canva/platform": "^2.1.0",
13
+ "@canva/platform": "^2.2.0",
14
14
  "@canva/user": "^2.1.0",
15
15
  "cookie-parser": "1.4.7",
16
16
  "react": "18.3.1",
@@ -64,7 +64,7 @@
64
64
  "url-loader": "4.1.1",
65
65
  "webpack": "5.97.1",
66
66
  "webpack-cli": "5.1.4",
67
- "webpack-dev-server": "5.2.0",
67
+ "webpack-dev-server": "5.2.2",
68
68
  "yargs": "17.7.2"
69
69
  },
70
70
  "keywords": [
@@ -199,6 +199,7 @@ function buildDevConfig(options?: DevConfig): {
199
199
 
200
200
  const { port, enableHmr, appOrigin, appId, enableHttps, certFile, keyFile } =
201
201
  options;
202
+ const host = "localhost";
202
203
 
203
204
  let devServer: DevServerConfiguration = {
204
205
  server: enableHttps
@@ -210,7 +211,8 @@ function buildDevConfig(options?: DevConfig): {
210
211
  },
211
212
  }
212
213
  : "http",
213
- host: "localhost",
214
+ host,
215
+ allowedHosts: [host],
214
216
  historyApiFallback: {
215
217
  rewrites: [{ from: /^\/$/, to: "/app.js" }],
216
218
  },
@@ -227,7 +229,7 @@ function buildDevConfig(options?: DevConfig): {
227
229
  if (enableHmr && appOrigin) {
228
230
  devServer = {
229
231
  ...devServer,
230
- allowedHosts: new URL(appOrigin).hostname,
232
+ allowedHosts: [host, new URL(appOrigin).hostname],
231
233
  headers: {
232
234
  "Access-Control-Allow-Origin": appOrigin,
233
235
  "Access-Control-Allow-Credentials": "true",
@@ -245,7 +247,7 @@ function buildDevConfig(options?: DevConfig): {
245
247
  const appDomain = `app-${appId.toLowerCase().trim()}.canva-apps.com`;
246
248
  devServer = {
247
249
  ...devServer,
248
- allowedHosts: appDomain,
250
+ allowedHosts: [host, appDomain],
249
251
  headers: {
250
252
  "Access-Control-Allow-Origin": `https://${appDomain}`,
251
253
  "Access-Control-Allow-Credentials": "true",
@@ -20,11 +20,11 @@
20
20
  "dependencies": {
21
21
  "@canva/app-components": "^1.3.0",
22
22
  "@canva/app-i18n-kit": "^1.0.2",
23
- "@canva/app-ui-kit": "^4.9.0",
23
+ "@canva/app-ui-kit": "^4.10.0",
24
24
  "@canva/asset": "^2.2.0",
25
- "@canva/design": "^2.4.1",
25
+ "@canva/design": "^2.6.0",
26
26
  "@canva/error": "^2.1.0",
27
- "@canva/platform": "^2.1.0",
27
+ "@canva/platform": "^2.2.0",
28
28
  "@canva/user": "^2.1.0",
29
29
  "cookie-parser": "1.4.7",
30
30
  "cors": "2.8.5",
@@ -89,7 +89,7 @@
89
89
  "url-loader": "4.1.1",
90
90
  "webpack": "5.97.1",
91
91
  "webpack-cli": "5.1.4",
92
- "webpack-dev-server": "5.2.0",
92
+ "webpack-dev-server": "5.2.2",
93
93
  "yargs": "17.7.2"
94
94
  }
95
95
  }
@@ -199,6 +199,7 @@ function buildDevConfig(options?: DevConfig): {
199
199
 
200
200
  const { port, enableHmr, appOrigin, appId, enableHttps, certFile, keyFile } =
201
201
  options;
202
+ const host = "localhost";
202
203
 
203
204
  let devServer: DevServerConfiguration = {
204
205
  server: enableHttps
@@ -210,7 +211,8 @@ function buildDevConfig(options?: DevConfig): {
210
211
  },
211
212
  }
212
213
  : "http",
213
- host: "localhost",
214
+ host,
215
+ allowedHosts: [host],
214
216
  historyApiFallback: {
215
217
  rewrites: [{ from: /^\/$/, to: "/app.js" }],
216
218
  },
@@ -227,7 +229,7 @@ function buildDevConfig(options?: DevConfig): {
227
229
  if (enableHmr && appOrigin) {
228
230
  devServer = {
229
231
  ...devServer,
230
- allowedHosts: new URL(appOrigin).hostname,
232
+ allowedHosts: [host, new URL(appOrigin).hostname],
231
233
  headers: {
232
234
  "Access-Control-Allow-Origin": appOrigin,
233
235
  "Access-Control-Allow-Credentials": "true",
@@ -245,7 +247,7 @@ function buildDevConfig(options?: DevConfig): {
245
247
  const appDomain = `app-${appId.toLowerCase().trim()}.canva-apps.com`;
246
248
  devServer = {
247
249
  ...devServer,
248
- allowedHosts: appDomain,
250
+ allowedHosts: [host, appDomain],
249
251
  headers: {
250
252
  "Access-Control-Allow-Origin": `https://${appDomain}`,
251
253
  "Access-Control-Allow-Credentials": "true",
@@ -20,12 +20,12 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@canva/app-i18n-kit": "^1.0.2",
23
- "@canva/app-ui-kit": "^4.9.0",
23
+ "@canva/app-ui-kit": "^4.10.0",
24
24
  "@canva/asset": "^2.2.0",
25
- "@canva/design": "^2.4.1",
25
+ "@canva/design": "^2.6.0",
26
26
  "@canva/error": "^2.1.0",
27
- "@canva/intents": "^0.0.0-beta.2",
28
- "@canva/platform": "^2.1.0",
27
+ "@canva/intents": "^2.0.0",
28
+ "@canva/platform": "^2.2.0",
29
29
  "@canva/user": "^2.1.0",
30
30
  "react": "18.3.1",
31
31
  "react-dom": "18.3.1",
@@ -85,7 +85,7 @@
85
85
  "url-loader": "4.1.1",
86
86
  "webpack": "5.97.1",
87
87
  "webpack-cli": "5.1.4",
88
- "webpack-dev-server": "5.2.0",
88
+ "webpack-dev-server": "5.2.2",
89
89
  "yargs": "17.7.2"
90
90
  }
91
91
  }
@@ -16,9 +16,11 @@ import { SelectField } from "src/components/inputs/select_field";
16
16
  import { dateCell, numberCell, stringCell } from "src/utils";
17
17
  import { Paths } from "src/routes";
18
18
  import { useNavigate } from "react-router-dom";
19
- import { useState } from "react";
19
+ import { useEffect, useState } from "react";
20
+ import { SearchFilter } from "src/components/inputs/search_filter";
20
21
 
21
22
  export interface DesignsDataSource extends DataSourceConfig {
23
+ query: string;
22
24
  ownership: "any" | "owned" | "shared";
23
25
  sort_by:
24
26
  | "relevance"
@@ -41,6 +43,7 @@ export const designsSource = new DataSourceHandler<
41
43
  >(
42
44
  {
43
45
  schema: "designs/v1",
46
+ query: "",
44
47
  ownership: "any",
45
48
  sort_by: "relevance",
46
49
  },
@@ -77,7 +80,14 @@ export const designsSource = new DataSourceHandler<
77
80
  rowLimit: number,
78
81
  signal: AbortSignal | undefined,
79
82
  ) =>
80
- getDesigns(authToken, rowLimit, signal, source.ownership, source.sort_by),
83
+ getDesigns(
84
+ authToken,
85
+ rowLimit,
86
+ signal,
87
+ source.query,
88
+ source.ownership,
89
+ source.sort_by,
90
+ ),
81
91
  DesignSelection,
82
92
  DesignsSourceConfig,
83
93
  );
@@ -86,15 +96,24 @@ export async function getDesigns(
86
96
  authToken: string,
87
97
  rowLimit: number,
88
98
  signal: AbortSignal | undefined,
99
+ query: string,
89
100
  ownership: string,
90
101
  sort_by: string,
91
102
  continuation?: string,
92
103
  allItems: CanvaDesign[] = [],
93
104
  ): Promise<CanvaDesign[]> {
94
105
  const baseUrl = `https://api.canva.com/rest/v1/designs`;
95
- const url = continuation
96
- ? `${baseUrl}?continuation=${continuation}`
97
- : `${baseUrl}?ownership=${ownership}&sort_by=${sort_by}`;
106
+ const params = new URLSearchParams();
107
+ if (continuation) {
108
+ params.set("continuation", continuation);
109
+ } else {
110
+ if (query) {
111
+ params.set("query", query);
112
+ }
113
+ params.set("ownership", ownership);
114
+ params.set("sort_by", sort_by);
115
+ }
116
+ const url = `${baseUrl}?${params.toString()}`;
98
117
 
99
118
  return fetch(url, {
100
119
  headers: {
@@ -118,6 +137,7 @@ export async function getDesigns(
118
137
  authToken,
119
138
  rowLimit,
120
139
  signal,
140
+ query,
121
141
  ownership,
122
142
  sort_by,
123
143
  data.continuation,
@@ -171,14 +191,29 @@ function DesignSelection() {
171
191
  function DesignsSourceConfig(sourceConfig: DesignsDataSource) {
172
192
  const intl = useIntl();
173
193
  const { loadDataSource } = useAppContext();
194
+ const [query, setQuery] = useState<string>(sourceConfig.query);
174
195
  const [ownership, setOwnership] = useState<string>(sourceConfig.ownership);
175
196
  const [sortOrder, setSortOrder] = useState<string>(sourceConfig.sort_by);
176
197
  const [isLoading, setIsLoading] = useState(false);
177
198
 
199
+ const [filterCount, setFilterCount] = useState(0);
200
+ useEffect(() => {
201
+ // Update the filter count based on the selected filters
202
+ // consider a filter to be applied if not set to the default value
203
+ setFilterCount(
204
+ (ownership !== "any" ? 1 : 0) + (sortOrder !== "relevance" ? 1 : 0),
205
+ );
206
+ }, [ownership, sortOrder]);
207
+ const resetFilters = () => {
208
+ setOwnership("any");
209
+ setSortOrder("relevance");
210
+ };
211
+
178
212
  const loadDesigns = async () => {
179
213
  setIsLoading(true);
180
214
  loadDataSource("Canva Designs", {
181
215
  schema: "designs/v1",
216
+ query,
182
217
  ownership,
183
218
  sort_by: sortOrder,
184
219
  } as DesignsDataSource).then(() => {
@@ -223,18 +258,26 @@ function DesignsSourceConfig(sourceConfig: DesignsDataSource) {
223
258
  showBack={true}
224
259
  />
225
260
 
226
- <SelectField
227
- label={intl.formatMessage(ownershipFilter.label)}
228
- options={ownershipOptions}
229
- value={ownership}
230
- onChange={setOwnership}
231
- />
232
- <SelectField
233
- label={intl.formatMessage(sortOrderField.label)}
234
- options={sortOrderOptions}
235
- value={sortOrder}
236
- onChange={setSortOrder}
237
- />
261
+ <SearchFilter
262
+ value={query}
263
+ onChange={setQuery}
264
+ filterCount={filterCount}
265
+ resetFilters={resetFilters}
266
+ >
267
+ <SelectField
268
+ label={intl.formatMessage(ownershipFilter.label)}
269
+ options={ownershipOptions}
270
+ value={ownership}
271
+ onChange={setOwnership}
272
+ />
273
+ <SelectField
274
+ label={intl.formatMessage(sortOrderField.label)}
275
+ options={sortOrderOptions}
276
+ value={sortOrder}
277
+ onChange={setSortOrder}
278
+ />
279
+ </SearchFilter>
280
+
238
281
  <Button
239
282
  variant="primary"
240
283
  loading={isLoading}
@@ -21,9 +21,11 @@ import { SelectField } from "src/components/inputs/select_field";
21
21
  import { Header } from "src/components";
22
22
  import { useNavigate } from "react-router-dom";
23
23
  import { Paths } from "src/routes";
24
- import { useState } from "react";
24
+ import { useEffect, useState } from "react";
25
+ import { SearchFilter } from "src/components/inputs/search_filter";
25
26
 
26
27
  export interface BrandTemplatesDataSource extends DataSourceConfig {
28
+ query: string;
27
29
  dataset: "any" | "non_empty" | "empty";
28
30
  ownership: "any" | "owned" | "shared";
29
31
  sort_by:
@@ -48,6 +50,7 @@ export const brandTemplatesSource = new DataSourceHandler<
48
50
  >(
49
51
  {
50
52
  schema: "brand_templates/v1",
53
+ query: "",
51
54
  dataset: "any",
52
55
  ownership: "any",
53
56
  sort_by: "relevance",
@@ -94,8 +97,9 @@ export const brandTemplatesSource = new DataSourceHandler<
94
97
  authToken,
95
98
  rowLimit,
96
99
  signal,
97
- source.dataset,
100
+ source.query,
98
101
  source.ownership,
102
+ source.dataset,
99
103
  source.sort_by,
100
104
  ),
101
105
  BrandTemplatesSelection,
@@ -106,6 +110,7 @@ export async function getBrandTemplates(
106
110
  authToken: string,
107
111
  rowLimit: number,
108
112
  signal: AbortSignal | undefined,
113
+ query: string,
109
114
  ownership: string,
110
115
  dataset: string,
111
116
  sort_by: string,
@@ -113,9 +118,19 @@ export async function getBrandTemplates(
113
118
  allItems: CanvaBrandTemplate[] = [],
114
119
  ): Promise<CanvaBrandTemplate[]> {
115
120
  const baseUrl = `https://api.canva.com/rest/v1/brand-templates`;
116
- const url = continuation
117
- ? `${baseUrl}?continuation=${continuation}`
118
- : `${baseUrl}?ownership=${ownership}&dataset=${dataset}&sort_by=${sort_by}`;
121
+
122
+ const params = new URLSearchParams();
123
+ if (continuation) {
124
+ params.set("continuation", continuation);
125
+ } else {
126
+ if (query) {
127
+ params.set("query", query);
128
+ }
129
+ params.set("ownership", ownership);
130
+ params.set("dataset", dataset);
131
+ params.set("sort_by", sort_by);
132
+ }
133
+ const url = `${baseUrl}?${params.toString()}`;
119
134
 
120
135
  return fetch(url, {
121
136
  headers: {
@@ -139,6 +154,7 @@ export async function getBrandTemplates(
139
154
  authToken,
140
155
  rowLimit,
141
156
  signal,
157
+ query,
142
158
  ownership,
143
159
  dataset,
144
160
  sort_by,
@@ -191,16 +207,34 @@ function BrandTemplatesSelection() {
191
207
  }
192
208
 
193
209
  function BrandTemplatesSourceConfig(sourceConfig: BrandTemplatesDataSource) {
210
+ const intl = useIntl();
194
211
  const { loadDataSource } = useAppContext();
212
+ const [query, setQuery] = useState<string>(sourceConfig.query);
195
213
  const [ownership, setOwnership] = useState<string>(sourceConfig.ownership);
196
214
  const [sortOrder, setSortOrder] = useState<string>(sourceConfig.sort_by);
197
215
  const [dataset, setDataset] = useState<string>(sourceConfig.dataset);
198
216
  const [isLoading, setIsLoading] = useState(false);
199
- const intl = useIntl();
217
+
218
+ const [filterCount, setFilterCount] = useState(0);
219
+ useEffect(() => {
220
+ // Update the filter count based on the selected filters
221
+ // consider a filter to be applied if not set to the default value
222
+ setFilterCount(
223
+ (ownership !== "any" ? 1 : 0) +
224
+ (dataset !== "any" ? 1 : 0) +
225
+ (sortOrder !== "relevance" ? 1 : 0),
226
+ );
227
+ }, [ownership, dataset, sortOrder]);
228
+ const resetFilters = () => {
229
+ setOwnership("any");
230
+ setDataset("any");
231
+ setSortOrder("relevance");
232
+ };
200
233
 
201
234
  const loadTemplates = async () => {
202
235
  loadDataSource("Canva Brand Templates", {
203
236
  schema: "brand_templates/v1",
237
+ query,
204
238
  ownership,
205
239
  dataset,
206
240
  sort_by: sortOrder,
@@ -251,24 +285,32 @@ function BrandTemplatesSourceConfig(sourceConfig: BrandTemplatesDataSource) {
251
285
  showBack={true}
252
286
  />
253
287
  <Rows spacing="2u">
254
- <SelectField
255
- label={intl.formatMessage(ownershipFilter.label)}
256
- options={ownershipOptions}
257
- value={ownership}
258
- onChange={setOwnership}
259
- />
260
- <SelectField
261
- label={intl.formatMessage(datasetFilter.label)}
262
- options={datasetOptions}
263
- value={dataset}
264
- onChange={setDataset}
265
- />
266
- <SelectField
267
- label={intl.formatMessage(sortOrderField.label)}
268
- options={sortOrderOptions}
269
- value={sortOrder}
270
- onChange={setSortOrder}
271
- />
288
+ <SearchFilter
289
+ value={query}
290
+ onChange={setQuery}
291
+ filterCount={filterCount}
292
+ resetFilters={resetFilters}
293
+ >
294
+ <SelectField
295
+ label={intl.formatMessage(ownershipFilter.label)}
296
+ options={ownershipOptions}
297
+ value={ownership}
298
+ onChange={setOwnership}
299
+ />
300
+ <SelectField
301
+ label={intl.formatMessage(datasetFilter.label)}
302
+ options={datasetOptions}
303
+ value={dataset}
304
+ onChange={setDataset}
305
+ />
306
+ <SelectField
307
+ label={intl.formatMessage(sortOrderField.label)}
308
+ options={sortOrderOptions}
309
+ value={sortOrder}
310
+ onChange={setSortOrder}
311
+ />
312
+ </SearchFilter>
313
+
272
314
  <Button
273
315
  variant="primary"
274
316
  loading={isLoading}
@@ -1,6 +1,6 @@
1
1
  import type {
2
- FetchDataTableParams,
3
- FetchDataTableResult,
2
+ GetDataTableRequest,
3
+ GetDataTableResponse,
4
4
  } from "@canva/intents/data";
5
5
  import { DATA_SOURCES } from "./data_sources";
6
6
  import {
@@ -13,16 +13,16 @@ import { DataAPIError } from ".";
13
13
 
14
14
  /**
15
15
  * This function handles parsing the data fetch parameters and calling the appropriate handler for the data source.
16
- * @param fetchParams
16
+ * @param request
17
17
  * @param authToken
18
18
  * @returns
19
19
  */
20
20
  export const buildDataTableResult = async (
21
- fetchParams: FetchDataTableParams,
21
+ request: GetDataTableRequest,
22
22
  authToken?: string,
23
- ): Promise<FetchDataTableResult> => {
24
- const source = JSON.parse(fetchParams.dataSourceRef.source);
25
- const rowLimit = fetchParams.limit.row - 1; // -1 for the header row
23
+ ): Promise<GetDataTableResponse> => {
24
+ const source = JSON.parse(request.dataSourceRef.source);
25
+ const rowLimit = request.limit.row - 1; // -1 for the header row
26
26
 
27
27
  const dataHandler = DATA_SOURCES.find((handler) =>
28
28
  handler.matchSource(source),
@@ -37,8 +37,12 @@ export const buildDataTableResult = async (
37
37
  source,
38
38
  authToken || "",
39
39
  rowLimit,
40
- fetchParams.signal,
40
+ request.signal,
41
41
  );
42
+ if (dataTable.rows.length === 0) {
43
+ // if the data table is empty, return an error to prompt the user to reconfigure the data source
44
+ return appError("No results found.");
45
+ }
42
46
  return completeDataTable(dataTable);
43
47
  } catch (error) {
44
48
  if (error instanceof DataAPIError) {
@@ -5,17 +5,13 @@ import { AppUiProvider } from "@canva/app-ui-kit";
5
5
  import { AppI18nProvider } from "@canva/app-i18n-kit";
6
6
  import { ErrorBoundary } from "react-error-boundary";
7
7
  import { ErrorPage } from "./pages";
8
- import type { RenderSelectionUiParams } from "@canva/intents/data";
8
+ import type { RenderSelectionUiRequest } from "@canva/intents/data";
9
9
 
10
- export const App = ({
11
- dataParams,
12
- }: {
13
- dataParams: RenderSelectionUiParams;
14
- }) => (
10
+ export const App = ({ request }: { request: RenderSelectionUiRequest }) => (
15
11
  <AppI18nProvider>
16
12
  <AppUiProvider>
17
13
  <ErrorBoundary fallback={<ErrorPage />}>
18
- <ContextProvider renderSelectionUiParams={dataParams}>
14
+ <ContextProvider renderSelectionUiRequest={request}>
19
15
  <RouterProvider router={createHashRouter(routes)} />
20
16
  </ContextProvider>
21
17
  </ErrorBoundary>
@@ -78,3 +78,22 @@ export const sortOrderField = defineMessages({
78
78
  description: "Option for sort order",
79
79
  },
80
80
  });
81
+
82
+ export const filterMenu = defineMessages({
83
+ search: {
84
+ defaultMessage: "Search",
85
+ description: "Label for a search input field",
86
+ },
87
+ clear: {
88
+ defaultMessage: "Clear all",
89
+ description: "Label for a button to clear all filters",
90
+ },
91
+ apply: {
92
+ defaultMessage: "Apply",
93
+ description: "Label for a button to apply filters",
94
+ },
95
+ count: {
96
+ defaultMessage: "Filter count",
97
+ description: "Label for the number of active filters",
98
+ },
99
+ });
@@ -0,0 +1,108 @@
1
+ import {
2
+ Badge,
3
+ Box,
4
+ Button,
5
+ Column,
6
+ Columns,
7
+ Flyout,
8
+ Rows,
9
+ SearchInputMenu,
10
+ SlidersIcon,
11
+ } from "@canva/app-ui-kit";
12
+ import { useState } from "react";
13
+ import { useIntl } from "react-intl";
14
+ import { filterMenu } from "./messages";
15
+
16
+ interface SearchFilterProps {
17
+ value: string;
18
+ onChange: (value: string) => void;
19
+ filterCount: number;
20
+ resetFilters: () => void;
21
+ children?: React.ReactNode;
22
+ }
23
+
24
+ export const SearchFilter = ({
25
+ value,
26
+ onChange,
27
+ filterCount,
28
+ resetFilters,
29
+ children,
30
+ }: SearchFilterProps) => {
31
+ const intl = useIntl();
32
+ const [triggerRef, setTriggerRef] = useState<HTMLDivElement | null>(null);
33
+ const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
34
+
35
+ const onFilterClick = () => {
36
+ setIsFilterMenuOpen(!isFilterMenuOpen);
37
+ };
38
+
39
+ const filterButton = (
40
+ <Button
41
+ size="small"
42
+ variant="tertiary"
43
+ icon={SlidersIcon}
44
+ onClick={onFilterClick}
45
+ />
46
+ );
47
+ return (
48
+ <>
49
+ <Box paddingStart="0.5u">
50
+ <SearchInputMenu
51
+ value={value}
52
+ placeholder={intl.formatMessage(filterMenu.search)}
53
+ onChange={(value) => onChange(value)}
54
+ onClear={() => onChange("")}
55
+ ref={setTriggerRef}
56
+ end={
57
+ filterCount === 0 ? (
58
+ filterButton
59
+ ) : (
60
+ <Badge
61
+ tone="assist"
62
+ wrapInset="0"
63
+ shape="circle"
64
+ text={filterCount.toString()}
65
+ ariaLabel={intl.formatMessage(filterMenu.count)}
66
+ >
67
+ {filterButton}
68
+ </Badge>
69
+ )
70
+ }
71
+ />
72
+ </Box>
73
+ <Flyout
74
+ open={isFilterMenuOpen}
75
+ onRequestClose={() => setIsFilterMenuOpen(false)}
76
+ width="trigger"
77
+ trigger={triggerRef}
78
+ placement="bottom-center"
79
+ footer={
80
+ <Box padding="2u" background="surface">
81
+ <Columns spacing="1u">
82
+ <Column>
83
+ <Button variant="secondary" onClick={resetFilters} stretch>
84
+ {intl.formatMessage(filterMenu.clear)}
85
+ </Button>
86
+ </Column>
87
+ <Column>
88
+ <Button
89
+ variant="primary"
90
+ onClick={() => {
91
+ setIsFilterMenuOpen(false);
92
+ }}
93
+ stretch
94
+ >
95
+ {intl.formatMessage(filterMenu.apply)}
96
+ </Button>
97
+ </Column>
98
+ </Columns>
99
+ </Box>
100
+ }
101
+ >
102
+ <Box padding="2u">
103
+ <Rows spacing="2u">{children}</Rows>
104
+ </Box>
105
+ </Flyout>
106
+ </>
107
+ );
108
+ };