exb-mdprint 1.0.0
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/README.md +23 -0
- package/config.json +1 -0
- package/doc/index.html +42 -0
- package/doc/styles.css +95 -0
- package/icon.svg +1 -0
- package/manifest.json +16 -0
- package/package.json +20 -0
- package/src/config.ts +16 -0
- package/src/runtime/formatUtils.ts +135 -0
- package/src/runtime/style.css +82 -0
- package/src/runtime/widget.tsx +396 -0
- package/src/setting/setting.tsx +165 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Markdown Print Widget
|
|
2
|
+
|
|
3
|
+
This widget uses the simplicity of the Markdown language, combined with a simple parser, to create a wicked fast feature report generation tool. The markdown print widget allows the GIS builder to create a feature report using similar syntax to the Survey123 Feature Report syntax, providing a recognizable user experience when developing your feature reports. Some of the more complex features of Esri's Feature Reports have not been replicated (Like conditional formatting and map embeds). However, what you may lose in the way of "nice to have" features of feature reports, you gain in printing speed, reliability, and end-user modification.
|
|
4
|
+
|
|
5
|
+
## User Experience
|
|
6
|
+
|
|
7
|
+
This widget aims to make the user experience as simple as possible. The builder will configure potentially multiple templates, which the end user can choose from. When the user selects a feature (Or multiple features), the print button will become active, and an indicator will appear with the number of features selected for that template. By default, the template editor is hidden, but if the user is advanced, and wants to edit the way the template would be generated, they can open the template markdown or css and edit it in their own session.
|
|
8
|
+
|
|
9
|
+
When prints are made, there are no service calls, no downtime, just reliable markdown to html formatting, printing directly in browser. No server is receiving your print job, and the only service that is needed is for the data to load into your application for you to select. This means speed, and reliability, which is paramount for workflows like a check-in process, large document generation for mailing, record generation for government documentation requirements, etc.
|
|
10
|
+
|
|
11
|
+
## Builder Experience
|
|
12
|
+
|
|
13
|
+
The builder shouldn't have a difficult time formatting this widget. Since Markdown is used, it simplifies the formatting requirements, meaning that you don't have to fiddle around with a table in word, or ensure that all of your text is properly slammed together. Markdown also supports html tags, so things like images can still be embedded into the generated reports, meaning logos can be used! Since CSS is also used for the formatting, the sky is the limit in terms of the styling that is desired. Using other sites, like [https://markdowntohtml.com/](https://markdowntohtml.com/), you can quickly see results for generating your markdown and css templates. If you're looking for css inspiration (Or just want something to start with), I highly recommend checking out Jason M's GitHub collection of a few css stylesheets that is publicly available, along with a quick [visualizer](https://jasonm23.github.io/markdown-css-themes/) of the themes in action.
|
|
14
|
+
|
|
15
|
+
## Credits
|
|
16
|
+
|
|
17
|
+
This widget would not be possible without a few external libraries.
|
|
18
|
+
|
|
19
|
+
- marked: Used for converting markdown into HTML. This library is doing the bulk of the work.
|
|
20
|
+
- DOMPurify: Used for taking the html generated by marked, and cleaning it up, preventing someone from injecting malicious code into a feature service, which could be exploited when a print is made on it.
|
|
21
|
+
- print-js: Used for taking the generated HTML, and opening a print dialog with a well formatted document, all in-browser.
|
|
22
|
+
|
|
23
|
+
These external libraries power ~50% of the functionality of this widget. The user interface, and builder capabilities are done through Jimu-ui, and Calcite Components, and the parsing functionality to convert feature service data into the feature report is where the bulk of the effort actually put into this widget went.
|
package/config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
package/doc/index.html
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<head>
|
|
2
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
3
|
+
|
|
4
|
+
<!--Import Calcite Stylesheet via CDN. This is easier than having to create react stuff for each static page-->
|
|
5
|
+
<script
|
|
6
|
+
type="module"
|
|
7
|
+
src="https://js.arcgis.com/calcite-components/3.2.1/calcite.esm.js"
|
|
8
|
+
></script>
|
|
9
|
+
|
|
10
|
+
<!--Import Noto Sans so that the website draws properly-->
|
|
11
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
12
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
13
|
+
<link
|
|
14
|
+
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wdth,wght@0,62.5..100,100..900;1,62.5..100,100..900&display=swap"
|
|
15
|
+
rel="stylesheet"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<!--Reference component stylesheet-->
|
|
19
|
+
<link rel="stylesheet" href="doc/styles.css" />
|
|
20
|
+
|
|
21
|
+
<!--Webpage Title-->
|
|
22
|
+
<title>Markdown Print Help</title>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div class="header noto-sans-heavy">
|
|
26
|
+
<h1>Print Widget</h1>
|
|
27
|
+
</div>
|
|
28
|
+
<p>
|
|
29
|
+
This widget is designed to print simple feature reports using
|
|
30
|
+
<a href="https://www.markdownguide.org/basic-syntax/">markdown</a>
|
|
31
|
+
configuration. The markdown configured in the settings can include fields
|
|
32
|
+
from the datasource, wrapped in ${}. You can also perform some simple
|
|
33
|
+
formatting on date or number fields, using the following syntax:<br />
|
|
34
|
+
Date: ${dateField | "M/D/YY h:mm A"} => 2/15/26 4:23 PM<br />
|
|
35
|
+
Number: ${numField | "#,###.00"} => 23,456.78<br /><br />
|
|
36
|
+
The formatting string uses Arcade syntax, similar to the syntax in the
|
|
37
|
+
<a
|
|
38
|
+
href="https://developers.arcgis.com/arcade/function-reference/text_functions/#text"
|
|
39
|
+
>Text() Arcade function</a
|
|
40
|
+
>
|
|
41
|
+
</p>
|
|
42
|
+
</body>
|
package/doc/styles.css
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: "Noto Sans", sans-serif;
|
|
3
|
+
|
|
4
|
+
--calcite-sans-family: 'Noto Sans', Arial, Helvetica, sans-serif;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body,
|
|
8
|
+
.calcite-mode-dark,
|
|
9
|
+
.calcite-mode-light {
|
|
10
|
+
font-family: 'Noto Sans', Arial, Helvetica, sans-serif !important;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.action-bar {
|
|
14
|
+
--calcite-action-bar-items-space: 15px;
|
|
15
|
+
--calcite-action-bar-size: 100%
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.menu-button {
|
|
19
|
+
height: 30px;
|
|
20
|
+
width: 30px;
|
|
21
|
+
padding-left: 15px;
|
|
22
|
+
padding-right: 15px;
|
|
23
|
+
padding-bottom: 15px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
calcite-button {
|
|
27
|
+
--calcite-button-corner-radius: 5em;
|
|
28
|
+
--calcite-button-text-color: '#ffffff';
|
|
29
|
+
--calcite-button-icon-color: '#ffffff'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
html {
|
|
33
|
+
font-size: 120%
|
|
34
|
+
/* Page scaling was too small */
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
background-color: #212121;
|
|
39
|
+
color: #ffffff;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
main {
|
|
43
|
+
width: calc(500px + 20%);
|
|
44
|
+
max-width: 90%;
|
|
45
|
+
margin: auto;
|
|
46
|
+
padding: 20px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
footer {
|
|
50
|
+
text-align: right;
|
|
51
|
+
font-size: smaller;
|
|
52
|
+
padding: 15px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.sub-header {
|
|
56
|
+
color: #ffffff;
|
|
57
|
+
background-color: #212121;
|
|
58
|
+
padding: 10px;
|
|
59
|
+
text-align: center;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.header {
|
|
63
|
+
color: #ffffff;
|
|
64
|
+
background-color: #212121;
|
|
65
|
+
text-align: center;
|
|
66
|
+
font-optical-sizing: auto;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
font-style: normal;
|
|
69
|
+
font-variation-settings: "wdth" 75;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.header h1 {
|
|
73
|
+
font-size: 50px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.header h2 {
|
|
77
|
+
font-size: 30px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.header h3 {
|
|
81
|
+
font-size: 20px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
a:link {
|
|
85
|
+
color: var(--calcite-color-text-link) /* Unvisited link color */
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
a:visited {
|
|
89
|
+
color: #DC90EE; /* Visited link color */
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
a:focus-visible {
|
|
93
|
+
outline: 2px solid var(--calcite-color-brand);
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
}
|
package/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="208" height="128" viewBox="0 0 208 128"><rect width="198" height="118" x="5" y="5" ry="10" stroke="#000" stroke-width="10" fill="none"/><path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z"/></svg>
|
package/manifest.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "MDPrint",
|
|
3
|
+
"label": "MDPrint",
|
|
4
|
+
"type": "widget",
|
|
5
|
+
"version": "1.19.0",
|
|
6
|
+
"exbVersion": "1.19.0",
|
|
7
|
+
"author": "Lucius Creamer",
|
|
8
|
+
"description": "This widget allows users to print feature reports from the ArcGIS Experience Builder, client-side.",
|
|
9
|
+
"dependency": ["jimu-arcgis"],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"translatedLocales": ["en"],
|
|
12
|
+
"defaultSize": {
|
|
13
|
+
"width": 400,
|
|
14
|
+
"height": 800
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exb-mdprint",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A widget for printing a feature report client-side.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Lucius Creamer",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"dompurify": "^3.3.1",
|
|
9
|
+
"marked": "^17.0.2",
|
|
10
|
+
"print-js": "^1.6.0"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"exb-widget",
|
|
14
|
+
"experience-builder",
|
|
15
|
+
"exb"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"url": "https://github.com/SunshineLuke90/widgets"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UseDataSource } from "jimu-core"
|
|
2
|
+
import type { ImmutableObject } from "seamless-immutable"
|
|
3
|
+
|
|
4
|
+
export interface PrintTemplate {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
markdown: string
|
|
8
|
+
css: string
|
|
9
|
+
useDataSources?: UseDataSource[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Config {
|
|
13
|
+
PrintTemplates: PrintTemplate[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type IMConfig = ImmutableObject<Config>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import DOMPurify from "dompurify"
|
|
2
|
+
import { marked } from "marked"
|
|
3
|
+
import printJS from "print-js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
A simple date formatting function that supports the same token formatting as ArcGIS Arcade, for example:
|
|
7
|
+
|
|
8
|
+
formatDate($feature.dateField, 'MMMMM D, YYYY') => January 1, 2024
|
|
9
|
+
|
|
10
|
+
formatDate($feature.dateField, 'MM/DD/YYYY') => 01/01/2024
|
|
11
|
+
|
|
12
|
+
formatDate($feature.dateField, 'YYYY-MM-DD HH:mm:ss') => 2024-01-01 13:00:00
|
|
13
|
+
*/
|
|
14
|
+
const formatDate = (date: Date, format: string) => {
|
|
15
|
+
const map = {
|
|
16
|
+
MMMMM: date.toLocaleString("default", { month: "long" }),
|
|
17
|
+
MMM: date.toLocaleString("default", { month: "short" }),
|
|
18
|
+
MM: ("0" + (date.getMonth() + 1)).slice(-2),
|
|
19
|
+
M: date.getMonth() + 1,
|
|
20
|
+
DDDD: date.toLocaleString("default", { weekday: "long" }),
|
|
21
|
+
DDD: date.toLocaleString("default", { weekday: "short" }),
|
|
22
|
+
DD: ("0" + date.getDate()).slice(-2),
|
|
23
|
+
D: date.getDate(),
|
|
24
|
+
YYYY: date.getFullYear(),
|
|
25
|
+
YY: date.getFullYear().toString().slice(-2),
|
|
26
|
+
hh: ("0" + (date.getHours() % 12 || 12)).slice(-2),
|
|
27
|
+
h: date.getHours() % 12 || 12,
|
|
28
|
+
HH: ("0" + date.getHours()).slice(-2),
|
|
29
|
+
H: date.getHours(),
|
|
30
|
+
mm: ("0" + date.getMinutes()).slice(-2),
|
|
31
|
+
ss: ("0" + date.getSeconds()).slice(-2),
|
|
32
|
+
A: date.getHours() >= 12 ? "PM" : "AM"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return format.replace(
|
|
36
|
+
/MMMMM|MMM|MM|M|DDDD|DDD|DD|D|YYYY|YY|hh|h|HH|H|mm|ss|A/g,
|
|
37
|
+
(matched) => map[matched]
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
A simple number formatting function that supports comma separators and fixed decimal places, slightly different than ArcGIS Arcade, for example:
|
|
43
|
+
|
|
44
|
+
formatNumber(1234567.89, '#,###.00') => 1,234,567.89
|
|
45
|
+
|
|
46
|
+
formatNumber(1234.5, '0.00') => 1234.50
|
|
47
|
+
|
|
48
|
+
formatNumber(1234.5678, '0.0') => 1234.6
|
|
49
|
+
*/
|
|
50
|
+
const formatNumber = (num: number, format: string) => {
|
|
51
|
+
// Example: '#,###.00'
|
|
52
|
+
const hasComma = format.includes(",")
|
|
53
|
+
const decimalMatches = format.match(/\.(\d+)/)
|
|
54
|
+
const decimalPlaces = decimalMatches ? decimalMatches[1].length : 0
|
|
55
|
+
|
|
56
|
+
let result = num.toFixed(decimalPlaces)
|
|
57
|
+
|
|
58
|
+
if (hasComma) {
|
|
59
|
+
const parts = result.split(".")
|
|
60
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
|
61
|
+
result = parts.join(".")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const applyFormat = (value: any, format: string) => {
|
|
68
|
+
// If no format is provided, just return the raw value (or a string version)
|
|
69
|
+
if (!format) return value ?? ""
|
|
70
|
+
|
|
71
|
+
const isDateFormat = /[YMDhms]/.test(format)
|
|
72
|
+
const isNumberFormat = /[#0]/.test(format)
|
|
73
|
+
|
|
74
|
+
// 2. Route based on intent
|
|
75
|
+
if (isDateFormat) {
|
|
76
|
+
// Convert number/string to Date object first
|
|
77
|
+
const date = new Date(Number(value) || value)
|
|
78
|
+
return formatDate(date, format)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isNumberFormat) {
|
|
82
|
+
const num = parseFloat(value)
|
|
83
|
+
return formatNumber(num, format)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return value ?? "" // Fallback for strings or unknown types
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handles the print workflow: replaces field variables in markdown, converts to HTML,
|
|
91
|
+
* sanitizes, and opens a print dialog.
|
|
92
|
+
*
|
|
93
|
+
* @param records - The data records (selected features) to print, one page per record.
|
|
94
|
+
* @param markdown - The markdown template string with ${fieldName} or ${fieldName|format} placeholders.
|
|
95
|
+
* @param css - The custom CSS to apply to the print output.
|
|
96
|
+
* @returns "Success" if the print dialog was opened, or an error message string.
|
|
97
|
+
*/
|
|
98
|
+
export const handlePrint = (
|
|
99
|
+
records: any[],
|
|
100
|
+
markdown: string,
|
|
101
|
+
css: string
|
|
102
|
+
): string => {
|
|
103
|
+
if (!records || records.length === 0) {
|
|
104
|
+
return "No features selected. Please select at least one feature before printing."
|
|
105
|
+
}
|
|
106
|
+
if (!markdown) {
|
|
107
|
+
return "No markdown content configured for this template."
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const pages: string[] = []
|
|
112
|
+
for (const feature of records) {
|
|
113
|
+
const result = markdown.replace(/\${(.*?)}/g, (_match, contents) => {
|
|
114
|
+
const [fieldName, ...formatParts] = contents.split("|")
|
|
115
|
+
const field = fieldName.trim()
|
|
116
|
+
const format = formatParts.length
|
|
117
|
+
? formatParts.join("|").trim().replace(/['"]/g, "")
|
|
118
|
+
: null
|
|
119
|
+
const out = applyFormat(feature.getData()[field], format) ?? ""
|
|
120
|
+
return out
|
|
121
|
+
})
|
|
122
|
+
const htmlOut = `<div class="markdown-content">${DOMPurify.sanitize(marked.parse(result, { async: false }))}</div>`
|
|
123
|
+
const formattedHtml = htmlOut.replace(/\n/g, "<br>")
|
|
124
|
+
pages.push(formattedHtml)
|
|
125
|
+
}
|
|
126
|
+
const combinedHtml = pages.join(
|
|
127
|
+
'<div style="page-break-after: always;"></div>'
|
|
128
|
+
)
|
|
129
|
+
const cleanCss = "@page { margin: 20px; } " + css.replace(/\n/g, "")
|
|
130
|
+
printJS({ printable: combinedHtml, type: "raw-html", style: cleanCss })
|
|
131
|
+
return "Success"
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return `Print failed: ${error instanceof Error ? error.message : String(error)}`
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
.widget-mdprint {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100%;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Scrollable area that fills remaining space above the button */
|
|
9
|
+
.widget-mdprint .mdprint-scroll {
|
|
10
|
+
overflow: auto;
|
|
11
|
+
flex: 1 1 auto;
|
|
12
|
+
min-height: 0;
|
|
13
|
+
padding-right: 16px;
|
|
14
|
+
padding-left: 16px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Make the calcite print button full width and stick to the bottom of the widget */
|
|
18
|
+
.widget-mdprint calcite-button {
|
|
19
|
+
width: 100%;
|
|
20
|
+
flex: 0 0 auto;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
position: sticky;
|
|
23
|
+
bottom: 0;
|
|
24
|
+
z-index: 3;
|
|
25
|
+
padding-right: 16px;
|
|
26
|
+
padding-left: 16px;
|
|
27
|
+
padding-bottom: 16px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Add a little bottom padding so content isn't flush against the button */
|
|
31
|
+
.widget-mdprint .mdprint-scroll > *:last-child {
|
|
32
|
+
padding-bottom: 12px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.widget-mdprint header {
|
|
36
|
+
border-bottom: 1px solid rgba(0,0,0,0.12);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.widget-mdprint h4 {
|
|
40
|
+
font-size: 16px;
|
|
41
|
+
font-weight: 500;
|
|
42
|
+
margin: 0;
|
|
43
|
+
padding-bottom: 10px;
|
|
44
|
+
padding-top: 10px;
|
|
45
|
+
padding-left: 12px;
|
|
46
|
+
padding-right: 12px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.widget-mdprint .mdprint-scroll {
|
|
50
|
+
padding-top: 8px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.mdprint-field-header {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: space-between;
|
|
57
|
+
font-weight: 400;
|
|
58
|
+
margin: 8px 0 0 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.mdprint-field-header .label {
|
|
62
|
+
margin: 0;
|
|
63
|
+
font-weight: 400;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.mdprint-field-header calcite-button,
|
|
67
|
+
.mdprint-field-header .expand-button {
|
|
68
|
+
flex: 0 0 auto;
|
|
69
|
+
margin-left: 8px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.widget-mdprint .text-truncate {
|
|
73
|
+
font-weight: 400
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.widget-mdprint textarea {
|
|
77
|
+
border-radius: 4px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.modal-content textarea {
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DataSource,
|
|
3
|
+
DataSourceComponent,
|
|
4
|
+
type FeatureLayerQueryParams,
|
|
5
|
+
React,
|
|
6
|
+
type AllWidgetProps,
|
|
7
|
+
type DataRecord,
|
|
8
|
+
type ImmutableObject,
|
|
9
|
+
Immutable
|
|
10
|
+
} from "jimu-core"
|
|
11
|
+
import type { IMConfig, PrintTemplate } from "../config"
|
|
12
|
+
import {
|
|
13
|
+
Button,
|
|
14
|
+
CollapsablePanel,
|
|
15
|
+
Icon,
|
|
16
|
+
Modal,
|
|
17
|
+
ModalBody,
|
|
18
|
+
ModalHeader,
|
|
19
|
+
Paper,
|
|
20
|
+
TextArea
|
|
21
|
+
} from "jimu-ui"
|
|
22
|
+
import {
|
|
23
|
+
CalciteAlert,
|
|
24
|
+
CalciteButton,
|
|
25
|
+
CalciteOption,
|
|
26
|
+
CalciteSelect
|
|
27
|
+
} from "calcite-components"
|
|
28
|
+
import { handlePrint } from "./formatUtils"
|
|
29
|
+
import "./style.css"
|
|
30
|
+
|
|
31
|
+
export default function Widget(props: AllWidgetProps<IMConfig>) {
|
|
32
|
+
const { config } = props
|
|
33
|
+
// Store datasources keyed by datasource ID to avoid duplicates
|
|
34
|
+
const [datasources, setDatasources] = React.useState<{
|
|
35
|
+
[dsId: string]: DataSource
|
|
36
|
+
}>({})
|
|
37
|
+
// Store selected records keyed by datasource ID
|
|
38
|
+
const [selectedByDsId, setSelectedByDsId] = React.useState<{
|
|
39
|
+
[dsId: string]: DataRecord[]
|
|
40
|
+
}>({})
|
|
41
|
+
const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(
|
|
42
|
+
config?.PrintTemplates?.[0]?.id || ""
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Runtime overrides for markdown and css, keyed by template ID
|
|
46
|
+
const [markdownOverrides, setMarkdownOverrides] = React.useState<{
|
|
47
|
+
[templateId: string]: string
|
|
48
|
+
}>({})
|
|
49
|
+
const [cssOverrides, setCssOverrides] = React.useState<{
|
|
50
|
+
[templateId: string]: string
|
|
51
|
+
}>({})
|
|
52
|
+
|
|
53
|
+
// Alert state
|
|
54
|
+
const [alertOpen, setAlertOpen] = React.useState(false)
|
|
55
|
+
const [alertMessage, setAlertMessage] = React.useState("")
|
|
56
|
+
|
|
57
|
+
// Modal state for markdown editor
|
|
58
|
+
const [mdModalOpen, setMdModalOpen] = React.useState(false)
|
|
59
|
+
const openMDModal = () => {
|
|
60
|
+
setMdModalOpen(true)
|
|
61
|
+
}
|
|
62
|
+
const closeMDModal = () => {
|
|
63
|
+
setMdModalOpen(false)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Modal state for css editor
|
|
67
|
+
const [cssModalOpen, setCssModalOpen] = React.useState(false)
|
|
68
|
+
const openCSSModal = () => {
|
|
69
|
+
setCssModalOpen(true)
|
|
70
|
+
}
|
|
71
|
+
const closeCSSModal = () => {
|
|
72
|
+
setCssModalOpen(false)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const showAlert = (message: string) => {
|
|
76
|
+
setAlertMessage(message)
|
|
77
|
+
setAlertOpen(true)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isConfigured =
|
|
81
|
+
config?.PrintTemplates &&
|
|
82
|
+
config.PrintTemplates.length > 0 &&
|
|
83
|
+
config.PrintTemplates.some(
|
|
84
|
+
(t) => t.useDataSources && t.useDataSources.length > 0
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Collect unique useDataSources across all templates (deduplicated by dataSourceId)
|
|
88
|
+
const uniqueUseDataSources = React.useMemo(() => {
|
|
89
|
+
if (!config?.PrintTemplates) return []
|
|
90
|
+
const seen = new Set<string>()
|
|
91
|
+
const result = []
|
|
92
|
+
for (const template of config.PrintTemplates) {
|
|
93
|
+
if (template.useDataSources && template.useDataSources.length > 0) {
|
|
94
|
+
const uds = template.useDataSources[0]
|
|
95
|
+
if (!seen.has(uds.dataSourceId)) {
|
|
96
|
+
seen.add(uds.dataSourceId)
|
|
97
|
+
result.push(uds)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result
|
|
102
|
+
}, [config?.PrintTemplates])
|
|
103
|
+
|
|
104
|
+
// Get the datasource ID for a given template
|
|
105
|
+
const getDsIdForTemplate = (
|
|
106
|
+
template: ImmutableObject<PrintTemplate>
|
|
107
|
+
): string | null => {
|
|
108
|
+
return template.useDataSources?.[0]?.dataSourceId ?? null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get selected records for a given template's datasource
|
|
112
|
+
const getSelectedRecordsForTemplate = (
|
|
113
|
+
template: ImmutableObject<PrintTemplate>
|
|
114
|
+
): DataRecord[] => {
|
|
115
|
+
const dsId = getDsIdForTemplate(template)
|
|
116
|
+
if (!dsId) return []
|
|
117
|
+
return selectedByDsId[dsId] || []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const doPrint = (template: ImmutableObject<PrintTemplate>) => {
|
|
121
|
+
const dsId = getDsIdForTemplate(template)
|
|
122
|
+
if (!dsId || !datasources[dsId]) {
|
|
123
|
+
showAlert("No data source available for this template.")
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
const records = selectedByDsId[dsId] || []
|
|
127
|
+
|
|
128
|
+
// Use runtime overrides if present, otherwise fall back to config values
|
|
129
|
+
const markdown =
|
|
130
|
+
markdownOverrides[template.id] !== undefined
|
|
131
|
+
? markdownOverrides[template.id]
|
|
132
|
+
: template.markdown
|
|
133
|
+
const css =
|
|
134
|
+
cssOverrides[template.id] !== undefined
|
|
135
|
+
? cssOverrides[template.id]
|
|
136
|
+
: template.css
|
|
137
|
+
|
|
138
|
+
const result = handlePrint(records, markdown, css)
|
|
139
|
+
if (result !== "Success") {
|
|
140
|
+
showAlert(result)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Count of selected features for the currently active template
|
|
145
|
+
const selectedCount = React.useMemo(() => {
|
|
146
|
+
const template = config?.PrintTemplates?.find(
|
|
147
|
+
(t) => t.id === selectedTemplateId
|
|
148
|
+
)
|
|
149
|
+
if (!template) return 0
|
|
150
|
+
const dsId = template.useDataSources?.[0]?.dataSourceId
|
|
151
|
+
if (!dsId) return 0
|
|
152
|
+
return (selectedByDsId[dsId] || []).length
|
|
153
|
+
}, [config?.PrintTemplates, selectedTemplateId, selectedByDsId])
|
|
154
|
+
|
|
155
|
+
if (!isConfigured) {
|
|
156
|
+
return (
|
|
157
|
+
<Paper
|
|
158
|
+
variant="flat"
|
|
159
|
+
shape="none"
|
|
160
|
+
className="widget-mdprint jimu-widget"
|
|
161
|
+
style={{ whiteSpace: "pre-wrap", padding: "16px" }}
|
|
162
|
+
>
|
|
163
|
+
<p>Please configure print templates in the widget settings.</p>
|
|
164
|
+
</Paper>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Paper
|
|
170
|
+
variant="flat"
|
|
171
|
+
shape="none"
|
|
172
|
+
className="widget-mdprint jimu-widget"
|
|
173
|
+
style={{ whiteSpace: "pre-wrap" }}
|
|
174
|
+
>
|
|
175
|
+
<header className="widget-header">
|
|
176
|
+
<h4>Markdown Printer</h4>
|
|
177
|
+
</header>
|
|
178
|
+
<div className="mdprint-scroll">
|
|
179
|
+
<span style={{ fontSize: "14px" }}>Template</span>
|
|
180
|
+
<CalciteSelect
|
|
181
|
+
label="Select Print Template"
|
|
182
|
+
scale="s"
|
|
183
|
+
value={selectedTemplateId}
|
|
184
|
+
onCalciteSelectChange={(e: any) => {
|
|
185
|
+
const templateId = (e.target as HTMLCalciteSelectElement).value
|
|
186
|
+
setSelectedTemplateId(templateId)
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{config.PrintTemplates.map(
|
|
190
|
+
(template: ImmutableObject<PrintTemplate>) => {
|
|
191
|
+
const records = getSelectedRecordsForTemplate(template)
|
|
192
|
+
return (
|
|
193
|
+
<CalciteOption key={template.id} value={template.id}>
|
|
194
|
+
{template.label +
|
|
195
|
+
(records.length > 0 ? ` (${records.length} selected)` : "")}
|
|
196
|
+
</CalciteOption>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
)}
|
|
200
|
+
</CalciteSelect>
|
|
201
|
+
{/* Show markdown and CSS editors for the currently selected template */}
|
|
202
|
+
{selectedTemplateId &&
|
|
203
|
+
config.PrintTemplates.map(
|
|
204
|
+
(template: ImmutableObject<PrintTemplate>) => {
|
|
205
|
+
if (template.id !== selectedTemplateId) return null
|
|
206
|
+
const currentMarkdown =
|
|
207
|
+
markdownOverrides[template.id] !== undefined
|
|
208
|
+
? markdownOverrides[template.id]
|
|
209
|
+
: template.markdown
|
|
210
|
+
const currentCss =
|
|
211
|
+
cssOverrides[template.id] !== undefined
|
|
212
|
+
? cssOverrides[template.id]
|
|
213
|
+
: template.css
|
|
214
|
+
return (
|
|
215
|
+
<CollapsablePanel
|
|
216
|
+
key={template.id}
|
|
217
|
+
label="Edit Template"
|
|
218
|
+
defaultIsOpen={false}
|
|
219
|
+
level={3}
|
|
220
|
+
>
|
|
221
|
+
<div className="mdprint-field-header">
|
|
222
|
+
<div className="label">Markdown Content</div>
|
|
223
|
+
<Button
|
|
224
|
+
icon={true}
|
|
225
|
+
className="expand-button"
|
|
226
|
+
size="sm"
|
|
227
|
+
variant="text"
|
|
228
|
+
onClick={openMDModal}
|
|
229
|
+
>
|
|
230
|
+
<Icon
|
|
231
|
+
size="m"
|
|
232
|
+
icon='<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M15 15h-4v-1h2.282l-3.633-3.584.767-.767L14 13.282V11h1zM5 1v1H2.718l3.633 3.584-.767.767L2 2.718V5H1V1z"></path></svg>'
|
|
233
|
+
></Icon>
|
|
234
|
+
</Button>
|
|
235
|
+
<Modal
|
|
236
|
+
centered
|
|
237
|
+
keyboard
|
|
238
|
+
scrollable
|
|
239
|
+
toggle={closeMDModal}
|
|
240
|
+
isOpen={mdModalOpen}
|
|
241
|
+
onRequestClose={closeMDModal}
|
|
242
|
+
>
|
|
243
|
+
<ModalHeader toggle={closeMDModal}>
|
|
244
|
+
Markdown Content
|
|
245
|
+
</ModalHeader>
|
|
246
|
+
<ModalBody>
|
|
247
|
+
<TextArea
|
|
248
|
+
height={window.innerHeight * 0.8}
|
|
249
|
+
value={currentMarkdown}
|
|
250
|
+
onChange={(e) => {
|
|
251
|
+
const val = e.currentTarget.value
|
|
252
|
+
setMarkdownOverrides((prev) => ({
|
|
253
|
+
...prev,
|
|
254
|
+
[template.id]: val
|
|
255
|
+
}))
|
|
256
|
+
}}
|
|
257
|
+
placeholder="Enter markdown content here"
|
|
258
|
+
/>
|
|
259
|
+
</ModalBody>
|
|
260
|
+
</Modal>
|
|
261
|
+
</div>
|
|
262
|
+
<TextArea
|
|
263
|
+
value={currentMarkdown}
|
|
264
|
+
height={100}
|
|
265
|
+
onChange={(e) => {
|
|
266
|
+
const val = e.currentTarget.value
|
|
267
|
+
setMarkdownOverrides((prev) => ({
|
|
268
|
+
...prev,
|
|
269
|
+
[template.id]: val
|
|
270
|
+
}))
|
|
271
|
+
}}
|
|
272
|
+
placeholder="Enter markdown content here"
|
|
273
|
+
/>
|
|
274
|
+
<div className="mdprint-field-header">
|
|
275
|
+
<div className="label">Custom CSS</div>
|
|
276
|
+
<Button
|
|
277
|
+
icon={true}
|
|
278
|
+
className="expand-button"
|
|
279
|
+
size="sm"
|
|
280
|
+
variant="text"
|
|
281
|
+
onClick={openCSSModal}
|
|
282
|
+
>
|
|
283
|
+
<Icon
|
|
284
|
+
size="m"
|
|
285
|
+
icon='<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M15 15h-4v-1h2.282l-3.633-3.584.767-.767L14 13.282V11h1zM5 1v1H2.718l3.633 3.584-.767.767L2 2.718V5H1V1z"></path></svg>'
|
|
286
|
+
></Icon>
|
|
287
|
+
</Button>
|
|
288
|
+
<Modal
|
|
289
|
+
centered
|
|
290
|
+
keyboard
|
|
291
|
+
scrollable
|
|
292
|
+
toggle={closeCSSModal}
|
|
293
|
+
isOpen={cssModalOpen}
|
|
294
|
+
onRequestClose={closeCSSModal}
|
|
295
|
+
>
|
|
296
|
+
<ModalHeader toggle={closeCSSModal}>
|
|
297
|
+
CSS Content
|
|
298
|
+
</ModalHeader>
|
|
299
|
+
<ModalBody>
|
|
300
|
+
<TextArea
|
|
301
|
+
height={window.innerHeight * 0.8}
|
|
302
|
+
value={currentCss}
|
|
303
|
+
onChange={(e) => {
|
|
304
|
+
const val = e.currentTarget.value
|
|
305
|
+
setCssOverrides((prev) => ({
|
|
306
|
+
...prev,
|
|
307
|
+
[template.id]: val
|
|
308
|
+
}))
|
|
309
|
+
}}
|
|
310
|
+
placeholder="Enter CSS content here"
|
|
311
|
+
/>
|
|
312
|
+
</ModalBody>
|
|
313
|
+
</Modal>
|
|
314
|
+
</div>
|
|
315
|
+
<TextArea
|
|
316
|
+
value={currentCss}
|
|
317
|
+
height={100}
|
|
318
|
+
onChange={(e) => {
|
|
319
|
+
const val = e.currentTarget.value
|
|
320
|
+
setCssOverrides((prev) => ({
|
|
321
|
+
...prev,
|
|
322
|
+
[template.id]: val
|
|
323
|
+
}))
|
|
324
|
+
}}
|
|
325
|
+
placeholder="Enter custom CSS here"
|
|
326
|
+
/>
|
|
327
|
+
</CollapsablePanel>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<CalciteButton
|
|
334
|
+
appearance="solid"
|
|
335
|
+
disabled={!selectedTemplateId || selectedCount === 0}
|
|
336
|
+
style={{ marginTop: "8px" }}
|
|
337
|
+
className="print-button"
|
|
338
|
+
onClick={() => {
|
|
339
|
+
const template = config.PrintTemplates.find(
|
|
340
|
+
(t) => t.id === selectedTemplateId
|
|
341
|
+
)
|
|
342
|
+
if (template) {
|
|
343
|
+
doPrint(Immutable(template))
|
|
344
|
+
}
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
Print {selectedCount > 0 ? `(${selectedCount} selected)` : ""}
|
|
348
|
+
</CalciteButton>
|
|
349
|
+
{/* Render a DataSourceComponent for each unique datasource */}
|
|
350
|
+
{uniqueUseDataSources.map((uds) => (
|
|
351
|
+
<DataSourceComponent
|
|
352
|
+
key={uds.dataSourceId}
|
|
353
|
+
useDataSource={uds}
|
|
354
|
+
query={
|
|
355
|
+
{
|
|
356
|
+
where: "1=1",
|
|
357
|
+
outFields: ["*"],
|
|
358
|
+
returnGeometry: true
|
|
359
|
+
} as FeatureLayerQueryParams
|
|
360
|
+
}
|
|
361
|
+
widgetId={props.id}
|
|
362
|
+
onDataSourceCreated={(ds: DataSource) => {
|
|
363
|
+
setDatasources((prev) => {
|
|
364
|
+
if (prev[ds.id]) return prev
|
|
365
|
+
return { ...prev, [ds.id]: ds }
|
|
366
|
+
})
|
|
367
|
+
}}
|
|
368
|
+
onSelectionChange={() => {
|
|
369
|
+
// Look up the ds from our stored datasources by the expected ID
|
|
370
|
+
const ds = datasources[uds.dataSourceId]
|
|
371
|
+
if (!ds) return
|
|
372
|
+
const records = ds.getSelectedRecords()
|
|
373
|
+
setSelectedByDsId((prev) => ({
|
|
374
|
+
...prev,
|
|
375
|
+
[ds.id]: records
|
|
376
|
+
}))
|
|
377
|
+
}}
|
|
378
|
+
/>
|
|
379
|
+
))}
|
|
380
|
+
<CalciteAlert
|
|
381
|
+
open={alertOpen || undefined}
|
|
382
|
+
kind="danger"
|
|
383
|
+
icon="exclamation-mark-triangle"
|
|
384
|
+
autoClose
|
|
385
|
+
autoCloseDuration="medium"
|
|
386
|
+
label="Error alert"
|
|
387
|
+
onCalciteAlertClose={() => {
|
|
388
|
+
setAlertOpen(false)
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
<div slot="title">Error</div>
|
|
392
|
+
<div slot="message">{alertMessage}</div>
|
|
393
|
+
</CalciteAlert>
|
|
394
|
+
</Paper>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
React,
|
|
3
|
+
Immutable,
|
|
4
|
+
type UseDataSource,
|
|
5
|
+
DataSourceTypes,
|
|
6
|
+
utils,
|
|
7
|
+
type ImmutableObject
|
|
8
|
+
} from "jimu-core"
|
|
9
|
+
import type { AllWidgetSettingProps } from "jimu-for-builder"
|
|
10
|
+
import { Button, TextInput, Tabs, Tab, TextArea } from "jimu-ui"
|
|
11
|
+
import { SettingRow, SettingSection } from "jimu-ui/advanced/setting-components"
|
|
12
|
+
import { DataSourceSelector } from "jimu-ui/advanced/data-source-selector"
|
|
13
|
+
import type { IMConfig, PrintTemplate } from "../config"
|
|
14
|
+
|
|
15
|
+
export default function Setting(props: AllWidgetSettingProps<IMConfig>) {
|
|
16
|
+
const { id, config } = props
|
|
17
|
+
const [activeTab, setActiveTab] = React.useState<string | undefined>(
|
|
18
|
+
config?.PrintTemplates?.[0]?.id
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// helper to get a mutable copy of templates
|
|
22
|
+
const getTemplates = () =>
|
|
23
|
+
config?.PrintTemplates
|
|
24
|
+
? config.PrintTemplates.asMutable({ deep: true })
|
|
25
|
+
: []
|
|
26
|
+
|
|
27
|
+
const updateTemplate = (templateId: string, newData: Partial<any>) => {
|
|
28
|
+
const arr = getTemplates()
|
|
29
|
+
const index = arr.findIndex((template: any) => template.id === templateId)
|
|
30
|
+
if (index === -1) return
|
|
31
|
+
arr[index] = { ...arr[index], ...newData }
|
|
32
|
+
const newConfig = (config || Immutable({})).set("PrintTemplates", arr)
|
|
33
|
+
props.onSettingChange({ id, config: newConfig })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const addPrintTemplate = () => {
|
|
37
|
+
const newTemplate = {
|
|
38
|
+
id: `${id}-template-${utils.getUUID()}`,
|
|
39
|
+
label: `Template ${config?.PrintTemplates?.length + 1 || 1}`,
|
|
40
|
+
markdown:
|
|
41
|
+
"# Markdown Title\n\nThis is an example of markdown content in the MDPrint widget. You can use **bold**, *italic*, and other markdown syntax to format your content.\n\n- Item 1\n- Item 2\n- Item 3\n\n[ArcGIS Experience Builder](https://experience.arcgis.com/)",
|
|
42
|
+
css: ".markdown-content {\n font-family: 'Noto Sans', Arial, Helvetica, sans-serif;\n color: #333;\n}\n\n.markdown-content h1 {\n color: #0078d4;\n}\n\n.markdown-content a {\n color: #0078d4;\n text-decoration: none;\n}\n\n.markdown-content a:hover {\n text-decoration: underline;\n}"
|
|
43
|
+
}
|
|
44
|
+
const arr = getTemplates()
|
|
45
|
+
arr.push(newTemplate)
|
|
46
|
+
const newConfig = (config || Immutable({})).set("PrintTemplates", arr)
|
|
47
|
+
props.onSettingChange({ id, config: newConfig })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const removePrintTemplate = (templateId: string) => {
|
|
51
|
+
const arr = getTemplates().filter((t: any) => t.id !== templateId)
|
|
52
|
+
const newConfig = (config || Immutable({})).set("PrintTemplates", arr)
|
|
53
|
+
props.onSettingChange({ id, config: newConfig })
|
|
54
|
+
}
|
|
55
|
+
/*
|
|
56
|
+
const onDataSourceChange = (useDataSources: UseDataSource[]) => {
|
|
57
|
+
props.onSettingChange({
|
|
58
|
+
id: props.id,
|
|
59
|
+
useDataSources: useDataSources
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
*/
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
<SettingSection title="Markdown Print Setup">
|
|
66
|
+
<SettingRow level={1} flow="wrap">
|
|
67
|
+
<Button type="secondary" onClick={addPrintTemplate}>
|
|
68
|
+
Add Print Template
|
|
69
|
+
</Button>
|
|
70
|
+
</SettingRow>
|
|
71
|
+
<SettingRow level={1} flow="wrap">
|
|
72
|
+
{config?.PrintTemplates && config.PrintTemplates.length > 0 && (
|
|
73
|
+
<Tabs
|
|
74
|
+
value={activeTab}
|
|
75
|
+
onChange={(id) => {
|
|
76
|
+
setActiveTab(id)
|
|
77
|
+
}}
|
|
78
|
+
type="tabs"
|
|
79
|
+
scrollable={true}
|
|
80
|
+
keepMount
|
|
81
|
+
style={{ margin: "0 8px" }}
|
|
82
|
+
onClose={(id) => {
|
|
83
|
+
removePrintTemplate(id)
|
|
84
|
+
}}
|
|
85
|
+
children={
|
|
86
|
+
config.PrintTemplates.map(
|
|
87
|
+
(template: ImmutableObject<PrintTemplate>) => (
|
|
88
|
+
<Tab
|
|
89
|
+
id={template.id}
|
|
90
|
+
key={template.id}
|
|
91
|
+
title={template.label}
|
|
92
|
+
closeable
|
|
93
|
+
>
|
|
94
|
+
<SettingRow label="Template Name" level={1} flow="wrap">
|
|
95
|
+
<TextInput
|
|
96
|
+
value={template.label}
|
|
97
|
+
onChange={(e) => {
|
|
98
|
+
updateTemplate(template.id, {
|
|
99
|
+
label: e.currentTarget.value
|
|
100
|
+
})
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
</SettingRow>
|
|
104
|
+
<SettingRow
|
|
105
|
+
label="Select Data Source"
|
|
106
|
+
level={1}
|
|
107
|
+
flow="wrap"
|
|
108
|
+
>
|
|
109
|
+
<DataSourceSelector
|
|
110
|
+
types={Immutable([DataSourceTypes.FeatureLayer])}
|
|
111
|
+
mustUseDataSource={true}
|
|
112
|
+
isMultiple={false}
|
|
113
|
+
useDataSources={template.useDataSources}
|
|
114
|
+
useDataSourcesEnabled={props.useDataSourcesEnabled}
|
|
115
|
+
onChange={(uds: UseDataSource[]) => {
|
|
116
|
+
updateTemplate(template.id, { useDataSources: uds })
|
|
117
|
+
}}
|
|
118
|
+
widgetId={props.id}
|
|
119
|
+
/>
|
|
120
|
+
</SettingRow>
|
|
121
|
+
<SettingRow
|
|
122
|
+
label="Markdown Content"
|
|
123
|
+
level={2}
|
|
124
|
+
flow="wrap"
|
|
125
|
+
>
|
|
126
|
+
<TextArea
|
|
127
|
+
value={template.markdown}
|
|
128
|
+
height={window.innerHeight * 0.4}
|
|
129
|
+
onChange={(e) => {
|
|
130
|
+
updateTemplate(template.id, {
|
|
131
|
+
markdown: e.currentTarget.value
|
|
132
|
+
})
|
|
133
|
+
}}
|
|
134
|
+
placeholder="Enter markdown content here"
|
|
135
|
+
/>
|
|
136
|
+
</SettingRow>
|
|
137
|
+
<SettingRow label="Custom CSS" level={2} flow="wrap">
|
|
138
|
+
<TextArea
|
|
139
|
+
value={template.css}
|
|
140
|
+
height={window.innerHeight * 0.4}
|
|
141
|
+
onChange={(e) => {
|
|
142
|
+
updateTemplate(template.id, {
|
|
143
|
+
css: e.currentTarget.value
|
|
144
|
+
})
|
|
145
|
+
}}
|
|
146
|
+
placeholder="Enter custom CSS here"
|
|
147
|
+
/>
|
|
148
|
+
</SettingRow>
|
|
149
|
+
</Tab>
|
|
150
|
+
)
|
|
151
|
+
) as any
|
|
152
|
+
}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
</SettingRow>
|
|
156
|
+
{!config?.PrintTemplates || config.PrintTemplates.length === 0 ? (
|
|
157
|
+
<div>
|
|
158
|
+
No print templates configured. Click "Add Print Template" to create
|
|
159
|
+
one.
|
|
160
|
+
</div>
|
|
161
|
+
) : null}
|
|
162
|
+
</SettingSection>
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|