Skip to content

Commit 06cfc11

Browse files
authored
feat: Add hyperlink support in Views table (#2925)
1 parent 06b644d commit 06cfc11

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
7979
- [Limitations](#limitations)
8080
- [CSV Export](#csv-export)
8181
- [Views](#views)
82+
- [View Table](#view-table)
83+
- [Pointer](#pointer)
84+
- [Link](#link)
8285
- [Contributing](#contributing)
8386

8487
# Getting Started
@@ -1253,12 +1256,76 @@ This feature allows you to change how a pointer is represented in the browser. B
12531256
This feature will take either selected rows or all rows of an individual class and saves them to a CSV file, which is then downloaded. CSV headers are added to the top of the file matching the column names.
12541257

12551258
> ⚠️ There is currently a 10,000 row limit when exporting all data. If more than 10,000 rows are present in the class, the CSV file will only contain 10,000 rows.
1259+
12561260
## Views
12571261

12581262
▶️ *Core > Views*
12591263

12601264
Views are saved queries that display aggregated data from your classes. Create a view by providing a name, selecting a class and defining an aggregation pipeline. Optionally enable the object counter to show how many items match the view. Saved views appear in the sidebar, where you can select, edit, or delete them.
12611265

1266+
> [!Caution]
1267+
> Values are generally rendered without sanitization in the resulting data table. If rendered values come from user input or untrusted data, make sure to remove potentially dangerous HTML or JavaScript, to prevent an attacker from injecting malicious code, to exploit vulnerabilities like Cross-Site Scripting (XSS).
1268+
1269+
### View Table
1270+
1271+
When designing the aggregation pipeline, consider that some values are rendered specially in the output table.
1272+
1273+
#### Pointer
1274+
1275+
Parse Object pointers are automatically displayed as links to the target object.
1276+
1277+
Example:
1278+
1279+
```json
1280+
{ "__type": "Pointer", "className": "_User", "objectId": "xWMyZ4YEGZ" }
1281+
```
1282+
1283+
#### Link
1284+
1285+
Links are rendered as hyperlinks that open in a new browser tab.
1286+
1287+
Example:
1288+
1289+
```json
1290+
{
1291+
"__type": "Link",
1292+
"url": "https://example.com",
1293+
"text": "Link"
1294+
}
1295+
```
1296+
1297+
Set `isRelativeUrl: true` when linking to another dashboard page, in which case the base URL for the relative URL will be `<PROTOCOL>://<HOST>/<MOUNT_PATH>/apps/<APP_NAME>/`. The key `isRelativeUrl` is optional and `false` by default.
1298+
1299+
Example:
1300+
1301+
```json
1302+
{
1303+
"__type": "Link",
1304+
"url": "browser/_Installation",
1305+
"isRelativeUrl": true,
1306+
"text": "Link"
1307+
}
1308+
```
1309+
1310+
A query part of the URL can be easily added using the `urlQuery` key which will automatically escape the query string.
1311+
1312+
Example:
1313+
1314+
```json
1315+
{
1316+
"__type": "Link",
1317+
"url": "browser/_Installation",
1318+
"urlQuery": "filters=[{\"field\":\"objectId\",\"constraint\":\"eq\",\"compareTo\":\"xWMyZ4YEGZ\",\"class\":\"_Installation\"}]",
1319+
"isRelativeUrl": true,
1320+
"text": "Link"
1321+
}
1322+
```
1323+
1324+
In the example above, the query string will be escaped and added to the url, resulting in the complete URL:
1325+
1326+
```js
1327+
"browser/_Installation?filters=%5B%7B%22field%22%3A%22objectId%22%2C%22constraint%22%3A%22eq%22%2C%22compareTo%22%3A%22xWMyZ4YEGZ%22%2C%22class%22%3A%22_Installation%22%7D%5D"
1328+
```
12621329

12631330
# Contributing
12641331

src/dashboard/Data/Views/Views.react.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,13 @@ class Views extends TableView {
132132
if (text === undefined) {
133133
text = '';
134134
} else if (text && typeof text === 'object') {
135-
text = text.__type === 'Date' && text.iso ? text.iso : JSON.stringify(text);
135+
if (text.__type === 'Date' && text.iso) {
136+
text = text.iso;
137+
} else if (text.__type === 'Link' && text.text) {
138+
text = text.text;
139+
} else {
140+
text = JSON.stringify(text);
141+
}
136142
}
137143
text = String(text);
138144
if (typeof document !== 'undefined') {
@@ -166,6 +172,8 @@ class Views extends TableView {
166172
type = 'File';
167173
} else if (val.__type === 'GeoPoint') {
168174
type = 'GeoPoint';
175+
} else if (val.__type === 'Link') {
176+
type = 'Link';
169177
} else {
170178
type = 'Object';
171179
}
@@ -285,6 +293,8 @@ class Views extends TableView {
285293
type = 'File';
286294
} else if (value.__type === 'GeoPoint') {
287295
type = 'GeoPoint';
296+
} else if (value.__type === 'Link') {
297+
type = 'Link';
288298
} else {
289299
type = 'Object';
290300
}
@@ -306,6 +316,34 @@ class Views extends TableView {
306316
content = JSON.stringify(value);
307317
} else if (type === 'Date') {
308318
content = value && value.iso ? value.iso : String(value);
319+
} else if (type === 'Link') {
320+
// Sanitize URL
321+
let url = value.url;
322+
if (
323+
url.match(/javascript/i) ||
324+
url.match(/<script/i)
325+
) {
326+
url = '#';
327+
} else {
328+
url = value.isRelativeUrl
329+
? `apps/${this.context.slug}/${url}${value.query ? `?${new URLSearchParams(value.urlQuery).toString()}` : ''}`
330+
: url;
331+
}
332+
// Sanitize text
333+
let text = value.text;
334+
if (
335+
text.match(/javascript/i) ||
336+
text.match(/<script/i) ||
337+
!text ||
338+
text.trim() === ''
339+
) {
340+
text = 'Link';
341+
}
342+
content = (
343+
<a href={url} target="_blank" rel="noreferrer">
344+
{text}
345+
</a>
346+
);
309347
} else if (value === undefined) {
310348
content = '';
311349
} else {

0 commit comments

Comments
 (0)