@firedrill-tools/netsuite 0.1.2
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/LICENSE +201 -0
- package/README.md +214 -0
- package/firedrill/agent.target.json +16 -0
- package/firedrill/baseline.scenario.json +6905 -0
- package/firedrill/concurrency-limited.scenario.json +11 -0
- package/firedrill/conformance.suite.json +21 -0
- package/firedrill/ns-analytics.drill.json +73 -0
- package/firedrill/ns-concurrency.drill.json +53 -0
- package/firedrill/ns-default-identity.drill.json +43 -0
- package/firedrill/ns-denied.drill.json +38 -0
- package/firedrill/ns-insufficient-permission.drill.json +53 -0
- package/firedrill/ns-invalid-login.drill.json +53 -0
- package/firedrill/ns-order-to-cash.drill.json +93 -0
- package/firedrill/ns-record-locked.drill.json +53 -0
- package/firedrill/ns-records.drill.json +68 -0
- package/firedrill/ns-role-ar-clerk.drill.json +58 -0
- package/firedrill/ns-role-sales-rep.drill.json +58 -0
- package/firedrill/ns-subsidiary-scope.drill.json +58 -0
- package/firedrill/ns-tight-limits.drill.json +58 -0
- package/firedrill/ns-transform-lost.drill.json +63 -0
- package/firedrill/ns-write-outage.drill.json +63 -0
- package/firedrill/record-locked.scenario.json +11 -0
- package/firedrill/tight-limits.scenario.json +17 -0
- package/firedrill/tools/netsuite/app/assets/ATTRIBUTION.md +37 -0
- package/firedrill/tools/netsuite/app/assets/fonts/LICENSE.txt +92 -0
- package/firedrill/tools/netsuite/app/assets/oracle-netsuite.svg +1 -0
- package/firedrill/tools/netsuite/app/assets/oracle.svg +1 -0
- package/firedrill/tools/netsuite/app/site/app.js +182 -0
- package/firedrill/tools/netsuite/app/site/assets/fonts/open-sans-latin-ext.woff2 +0 -0
- package/firedrill/tools/netsuite/app/site/assets/fonts/open-sans-latin.woff2 +0 -0
- package/firedrill/tools/netsuite/app/site/assets/oracle-netsuite.svg +1 -0
- package/firedrill/tools/netsuite/app/site/assets/oracle.svg +1 -0
- package/firedrill/tools/netsuite/app/site/icons.js +59 -0
- package/firedrill/tools/netsuite/app/site/index.html +68 -0
- package/firedrill/tools/netsuite/app/site/menus.js +111 -0
- package/firedrill/tools/netsuite/app/site/overlay.js +121 -0
- package/firedrill/tools/netsuite/app/site/store.js +95 -0
- package/firedrill/tools/netsuite/app/site/styles/base.css +131 -0
- package/firedrill/tools/netsuite/app/site/styles/chrome.css +183 -0
- package/firedrill/tools/netsuite/app/site/styles/pages.css +134 -0
- package/firedrill/tools/netsuite/app/site/styles/tables.css +92 -0
- package/firedrill/tools/netsuite/app/site/ui.js +119 -0
- package/firedrill/tools/netsuite/app/site/views/common.js +134 -0
- package/firedrill/tools/netsuite/app/site/views/customer-form.js +152 -0
- package/firedrill/tools/netsuite/app/site/views/customer.js +147 -0
- package/firedrill/tools/netsuite/app/site/views/customers.js +111 -0
- package/firedrill/tools/netsuite/app/site/views/home-chrome.js +75 -0
- package/firedrill/tools/netsuite/app/site/views/home-data.js +153 -0
- package/firedrill/tools/netsuite/app/site/views/home.js +162 -0
- package/firedrill/tools/netsuite/app/site/views/invoice-new.js +131 -0
- package/firedrill/tools/netsuite/app/site/views/invoice.js +232 -0
- package/firedrill/tools/netsuite/app/site/views/invoices.js +99 -0
- package/firedrill/tools/netsuite/app/site/views/items.js +95 -0
- package/firedrill/tools/netsuite/app/site/views/order-new.js +154 -0
- package/firedrill/tools/netsuite/app/site/views/order.js +214 -0
- package/firedrill/tools/netsuite/app/site/views/orders.js +93 -0
- package/firedrill/tools/netsuite/app/site/views/payments.js +65 -0
- package/firedrill/tools/netsuite/app/site/views/pickers.js +117 -0
- package/firedrill/tools/netsuite/app/site/views/subsidiaries.js +40 -0
- package/firedrill/tools/netsuite/app/site/views/suiteql.js +81 -0
- package/firedrill/tools/netsuite/behavior.mjs +13 -0
- package/firedrill/tools/netsuite/lib/body.mjs +185 -0
- package/firedrill/tools/netsuite/lib/collections.mjs +202 -0
- package/firedrill/tools/netsuite/lib/errors.mjs +91 -0
- package/firedrill/tools/netsuite/lib/handlers-meta.mjs +100 -0
- package/firedrill/tools/netsuite/lib/handlers-read.mjs +141 -0
- package/firedrill/tools/netsuite/lib/handlers-session.mjs +52 -0
- package/firedrill/tools/netsuite/lib/handlers-write.mjs +6 -0
- package/firedrill/tools/netsuite/lib/metadata.mjs +148 -0
- package/firedrill/tools/netsuite/lib/primitives.mjs +105 -0
- package/firedrill/tools/netsuite/lib/project-tran.mjs +153 -0
- package/firedrill/tools/netsuite/lib/project.mjs +178 -0
- package/firedrill/tools/netsuite/lib/q.mjs +280 -0
- package/firedrill/tools/netsuite/lib/session.mjs +188 -0
- package/firedrill/tools/netsuite/lib/suiteql-parse.mjs +382 -0
- package/firedrill/tools/netsuite/lib/suiteql-run.mjs +270 -0
- package/firedrill/tools/netsuite/lib/suiteql-select.mjs +118 -0
- package/firedrill/tools/netsuite/lib/suiteql-tables.mjs +162 -0
- package/firedrill/tools/netsuite/lib/tran.mjs +133 -0
- package/firedrill/tools/netsuite/lib/wire-decode.mjs +107 -0
- package/firedrill/tools/netsuite/lib/wire.mjs +228 -0
- package/firedrill/tools/netsuite/lib/write-customer.mjs +181 -0
- package/firedrill/tools/netsuite/lib/write-invoice.mjs +319 -0
- package/firedrill/tools/netsuite/lib/write-order.mjs +269 -0
- package/firedrill/tools/netsuite/netsuite.tool.json +4888 -0
- package/firedrill/transform-response-lost.scenario.json +11 -0
- package/firedrill/world.json +7711 -0
- package/firedrill/write-outage.scenario.json +11 -0
- package/firedrill.json +5 -0
- package/package.json +52 -0
- package/starter.json +6904 -0
- package/test/conformance.mjs +701 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* The persistent NetSuite frame: application bar, global menu bar, account strip, menus, dialogs, footer. */
|
|
2
|
+
.appbar {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: 14px;
|
|
6
|
+
background: var(--bar);
|
|
7
|
+
color: #fff;
|
|
8
|
+
padding: 0 14px;
|
|
9
|
+
height: 54px;
|
|
10
|
+
flex: 0 0 auto;
|
|
11
|
+
}
|
|
12
|
+
.brand { display: flex; align-items: center; height: 54px; overflow: hidden; flex: 0 0 auto; }
|
|
13
|
+
.brand img { height: 68px; width: 68px; margin: -7px 0; filter: invert(1) brightness(1.9); }
|
|
14
|
+
.brand:hover { text-decoration: none; }
|
|
15
|
+
|
|
16
|
+
.appbar-right { margin-left: auto; display: flex; align-items: center; gap: 4px; }
|
|
17
|
+
|
|
18
|
+
.global-search { position: relative; display: flex; align-items: center; }
|
|
19
|
+
.global-search input {
|
|
20
|
+
width: 210px;
|
|
21
|
+
min-height: 24px;
|
|
22
|
+
background: #0f1d2e;
|
|
23
|
+
border: 1px solid #3c5474;
|
|
24
|
+
color: #fff;
|
|
25
|
+
padding-left: 26px;
|
|
26
|
+
}
|
|
27
|
+
.global-search input::placeholder { color: #93a4b7; }
|
|
28
|
+
.gs-go {
|
|
29
|
+
position: absolute; left: 4px; top: 50%; transform: translateY(-50%);
|
|
30
|
+
background: none; border: none; cursor: pointer; color: #93a4b7; display: flex; padding: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.bar-btn, .user-btn {
|
|
34
|
+
background: none;
|
|
35
|
+
border: none;
|
|
36
|
+
color: #dfe7f0;
|
|
37
|
+
font: inherit;
|
|
38
|
+
font-size: 11px;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
padding: 5px 7px;
|
|
41
|
+
border-radius: var(--radius);
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 5px;
|
|
45
|
+
}
|
|
46
|
+
.bar-btn:hover, .user-btn:hover, .bar-btn.open, .user-btn.open { background: rgba(255, 255, 255, 0.14); color: #fff; }
|
|
47
|
+
.bar-label { display: none; }
|
|
48
|
+
.user-btn { padding-left: 6px; }
|
|
49
|
+
.avatar {
|
|
50
|
+
width: 24px; height: 24px; border-radius: 50%;
|
|
51
|
+
background: #4a7bb5; color: #fff; font-size: 10px; font-weight: 700;
|
|
52
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
53
|
+
}
|
|
54
|
+
.user-lines { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; }
|
|
55
|
+
.user-name { font-weight: 600; color: #fff; }
|
|
56
|
+
.user-role { font-size: 10px; color: #a9bbcd; }
|
|
57
|
+
|
|
58
|
+
.nav-toggle { display: none; background: none; border: none; color: #fff; cursor: pointer; padding: 4px; }
|
|
59
|
+
|
|
60
|
+
/* ---- global menu bar ---- */
|
|
61
|
+
.menubar {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: stretch;
|
|
64
|
+
background: var(--menu);
|
|
65
|
+
flex: 0 0 auto;
|
|
66
|
+
padding: 0 6px;
|
|
67
|
+
overflow-x: auto;
|
|
68
|
+
scrollbar-width: none;
|
|
69
|
+
}
|
|
70
|
+
.menubar::-webkit-scrollbar { display: none; }
|
|
71
|
+
.menubar .tab {
|
|
72
|
+
background: none;
|
|
73
|
+
border: none;
|
|
74
|
+
color: #eaf1f9;
|
|
75
|
+
font: inherit;
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
padding: 7px 11px;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
}
|
|
81
|
+
.menubar .tab:hover, .menubar .tab.open { background: var(--menu-hover); color: #fff; }
|
|
82
|
+
.menubar .tab.active { background: #1c3a5e; color: #fff; font-weight: 600; }
|
|
83
|
+
|
|
84
|
+
/* ---- account strip ---- */
|
|
85
|
+
.account-strip {
|
|
86
|
+
display: flex; align-items: center; gap: 8px;
|
|
87
|
+
background: #dbe3ec;
|
|
88
|
+
border-bottom: 1px solid #b7c2ce;
|
|
89
|
+
padding: 3px 14px;
|
|
90
|
+
font-size: 11px;
|
|
91
|
+
color: #33414f;
|
|
92
|
+
flex: 0 0 auto;
|
|
93
|
+
}
|
|
94
|
+
.acct-name { font-weight: 700; }
|
|
95
|
+
.acct-sep { color: #8b98a6; }
|
|
96
|
+
.acct-role { background: none; border: none; font: inherit; color: var(--link); cursor: pointer; padding: 0; }
|
|
97
|
+
.acct-role:hover { text-decoration: underline; }
|
|
98
|
+
.acct-chip {
|
|
99
|
+
border: 1px solid #9fb4cb; background: #eaf1f8; color: #2c4f7c;
|
|
100
|
+
border-radius: 9px; padding: 0 8px; font-size: 10px; font-weight: 600;
|
|
101
|
+
}
|
|
102
|
+
.acct-date { margin-left: auto; color: #4d5a68; }
|
|
103
|
+
|
|
104
|
+
.page { flex: 1 1 auto; padding: 12px 14px 22px; outline: none; }
|
|
105
|
+
|
|
106
|
+
/* ---- footer ---- */
|
|
107
|
+
.ns-footer {
|
|
108
|
+
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
109
|
+
border-top: 1px solid #c3cbd6;
|
|
110
|
+
background: #e4e8ee;
|
|
111
|
+
padding: 6px 14px;
|
|
112
|
+
font-size: 10px;
|
|
113
|
+
color: #56616e;
|
|
114
|
+
flex: 0 0 auto;
|
|
115
|
+
}
|
|
116
|
+
.foot-links { display: flex; gap: 10px; }
|
|
117
|
+
.foot-link { background: none; border: none; color: var(--link); font: inherit; cursor: pointer; padding: 0; }
|
|
118
|
+
.foot-link:hover { text-decoration: underline; }
|
|
119
|
+
.foot-note { margin-left: auto; }
|
|
120
|
+
|
|
121
|
+
/* ---- dropdown menus ---- */
|
|
122
|
+
.menu-pop {
|
|
123
|
+
position: fixed;
|
|
124
|
+
z-index: 30;
|
|
125
|
+
min-width: 210px;
|
|
126
|
+
max-width: 320px;
|
|
127
|
+
background: #fff;
|
|
128
|
+
border: 1px solid #93a3b4;
|
|
129
|
+
box-shadow: 0 6px 18px rgba(16, 32, 52, 0.22);
|
|
130
|
+
padding: 3px 0;
|
|
131
|
+
}
|
|
132
|
+
.menu-head { padding: 5px 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; color: #6b7683; letter-spacing: 0.04em; }
|
|
133
|
+
.menu-divider { height: 1px; background: #dde2e9; margin: 3px 0; }
|
|
134
|
+
.mi {
|
|
135
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%;
|
|
136
|
+
background: none; border: none; font: inherit; font-size: 12px; text-align: left;
|
|
137
|
+
padding: 4px 12px; cursor: pointer; color: var(--ink);
|
|
138
|
+
}
|
|
139
|
+
.mi:hover:not(:disabled) { background: var(--menu-hover); color: #fff; }
|
|
140
|
+
.mi:disabled { color: #9aa5b1; cursor: default; }
|
|
141
|
+
.mi-ns { color: #4a5663; }
|
|
142
|
+
.mi-hint { font-size: 10px; color: #8b97a4; }
|
|
143
|
+
.mi:hover .mi-hint { color: #d9e4f0; }
|
|
144
|
+
|
|
145
|
+
/* ---- dialogs ---- */
|
|
146
|
+
.scrim {
|
|
147
|
+
position: fixed; inset: 0; z-index: 50;
|
|
148
|
+
background: rgba(16, 26, 38, 0.45);
|
|
149
|
+
display: flex; align-items: flex-start; justify-content: center;
|
|
150
|
+
padding: 60px 16px 24px;
|
|
151
|
+
overflow: auto;
|
|
152
|
+
}
|
|
153
|
+
.dialog { background: #fff; border: 1px solid #7c8a99; width: min(520px, 100%); box-shadow: 0 12px 34px rgba(0, 0, 0, 0.3); }
|
|
154
|
+
.dialog.wide { width: min(820px, 100%); }
|
|
155
|
+
.dialog-head {
|
|
156
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
157
|
+
background: linear-gradient(#3f76b4, #28598f); color: #fff; padding: 6px 10px;
|
|
158
|
+
}
|
|
159
|
+
.dialog-head h2 { margin: 0; font-size: 13px; font-weight: 700; }
|
|
160
|
+
.dialog-head .icon-btn { color: #fff; }
|
|
161
|
+
.dialog-body { padding: 12px; }
|
|
162
|
+
.dialog-foot { display: flex; justify-content: flex-end; gap: 6px; padding: 8px 10px; background: #eef1f5; border-top: 1px solid var(--line-soft); }
|
|
163
|
+
|
|
164
|
+
/* ---- toast ---- */
|
|
165
|
+
.toast {
|
|
166
|
+
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
|
|
167
|
+
z-index: 60; display: flex; align-items: center; gap: 8px;
|
|
168
|
+
padding: 8px 14px; font-size: 12px; border: 1px solid; border-radius: var(--radius);
|
|
169
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
170
|
+
}
|
|
171
|
+
.toast[hidden] { display: none; }
|
|
172
|
+
.toast.success { background: #e7f4ea; border-color: #9ccbab; color: #14532b; }
|
|
173
|
+
.toast.error { background: #fdecea; border-color: #e0a7a3; color: #7d1a17; }
|
|
174
|
+
.toast button { background: none; border: none; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; }
|
|
175
|
+
|
|
176
|
+
@media (max-width: 860px) {
|
|
177
|
+
.nav-toggle { display: inline-flex; }
|
|
178
|
+
.menubar { display: none; }
|
|
179
|
+
.appbar.nav-open + .menubar { display: block; }
|
|
180
|
+
.appbar.nav-open + .menubar .tab { display: block; width: 100%; text-align: left; }
|
|
181
|
+
.global-search input { width: 130px; }
|
|
182
|
+
.user-lines { display: none; }
|
|
183
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/* Dashboard portlets, record pages (header band, field columns, subtabs, sublists) and the SuiteQL tool. */
|
|
2
|
+
.dash { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; align-items: start; }
|
|
3
|
+
.dash .col { display: flex; flex-direction: column; gap: 12px; }
|
|
4
|
+
.dash-title { grid-column: 1 / -1; display: flex; align-items: baseline; gap: 10px; margin: 0 0 2px; }
|
|
5
|
+
.dash-title h1 { margin: 0; font-size: 17px; font-weight: 700; color: #1b2a3a; }
|
|
6
|
+
|
|
7
|
+
.portlet { background: var(--panel); border: 1px solid var(--line); }
|
|
8
|
+
.portlet > header {
|
|
9
|
+
display: flex; align-items: center; gap: 8px;
|
|
10
|
+
background: linear-gradient(#5d84b0, #416a97);
|
|
11
|
+
color: #fff; padding: 4px 9px;
|
|
12
|
+
}
|
|
13
|
+
.portlet > header h2 { margin: 0; font-size: 12px; font-weight: 700; }
|
|
14
|
+
.portlet > header .phead-actions { margin-left: auto; display: flex; gap: 2px; }
|
|
15
|
+
.portlet > header .icon-btn { color: #dfe9f4; }
|
|
16
|
+
.portlet .pbody { padding: 8px 10px; }
|
|
17
|
+
.portlet table.kv { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
18
|
+
.portlet table.kv td { padding: 3px 0; border-bottom: 1px solid #edf0f4; }
|
|
19
|
+
.portlet table.kv td.num { text-align: right; font-weight: 700; }
|
|
20
|
+
.portlet table.kv tr:last-child td { border-bottom: none; }
|
|
21
|
+
.reminder { display: flex; align-items: center; gap: 8px; padding: 4px 0; border-bottom: 1px solid #edf0f4; }
|
|
22
|
+
.reminder .rcount {
|
|
23
|
+
min-width: 30px; text-align: center; font-weight: 700; color: #fff;
|
|
24
|
+
background: #416a97; border-radius: 2px; padding: 1px 5px;
|
|
25
|
+
}
|
|
26
|
+
.reminder .rcount.alert { background: var(--red); }
|
|
27
|
+
.reminder .rcount.zero { background: #9aa6b3; }
|
|
28
|
+
.shortcut-list { list-style: none; margin: 0; padding: 0; }
|
|
29
|
+
.shortcut-list li { padding: 3px 0; border-bottom: 1px solid #edf0f4; }
|
|
30
|
+
.shortcut-list li:last-child { border-bottom: none; }
|
|
31
|
+
|
|
32
|
+
/* ---- record pages ---- */
|
|
33
|
+
.record { background: var(--panel); border: 1px solid var(--line); }
|
|
34
|
+
.record-head {
|
|
35
|
+
background: linear-gradient(#e9eff6, #d8e2ec);
|
|
36
|
+
border-bottom: 1px solid var(--line);
|
|
37
|
+
padding: 8px 10px;
|
|
38
|
+
display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap;
|
|
39
|
+
}
|
|
40
|
+
.record-head .rh-title { display: flex; flex-direction: column; gap: 2px; }
|
|
41
|
+
.record-head h1 { margin: 0; font-size: 16px; font-weight: 700; color: #16283c; }
|
|
42
|
+
.record-head .rh-sub { font-size: 11px; color: #46525f; }
|
|
43
|
+
.record-head .rh-actions { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; }
|
|
44
|
+
|
|
45
|
+
.subtabs { display: flex; gap: 2px; background: var(--band); border-bottom: 1px solid var(--line); padding: 0 8px; overflow-x: auto; }
|
|
46
|
+
.subtabs button {
|
|
47
|
+
background: #cfdbe7; border: 1px solid #b4c3d2; border-bottom: none;
|
|
48
|
+
font: inherit; font-size: 11px; color: #2b3a49; padding: 4px 12px; cursor: pointer;
|
|
49
|
+
margin-top: 4px; border-radius: 2px 2px 0 0; white-space: nowrap;
|
|
50
|
+
}
|
|
51
|
+
.subtabs button:hover { background: #dbe5ee; }
|
|
52
|
+
.subtabs button[aria-selected="true"] { background: #fff; color: #12344f; font-weight: 700; border-color: var(--line); }
|
|
53
|
+
|
|
54
|
+
.record-body { padding: 12px; }
|
|
55
|
+
.fieldgroup { border: 1px solid var(--line-soft); margin-bottom: 12px; }
|
|
56
|
+
.fieldgroup > h3 {
|
|
57
|
+
margin: 0; font-size: 11px; font-weight: 700; color: #2b3a49; text-transform: uppercase; letter-spacing: 0.03em;
|
|
58
|
+
background: var(--band-2); border-bottom: 1px solid var(--line-soft); padding: 4px 9px;
|
|
59
|
+
}
|
|
60
|
+
.fieldgroup .fg-body { padding: 10px; }
|
|
61
|
+
.fieldcols { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 4px 26px; }
|
|
62
|
+
.kvrow { display: flex; gap: 8px; padding: 3px 0; border-bottom: 1px solid #f0f2f5; }
|
|
63
|
+
.kvrow dt { flex: 0 0 42%; color: #4b5763; font-weight: 600; margin: 0; }
|
|
64
|
+
.kvrow dd { margin: 0; flex: 1 1 auto; word-break: break-word; }
|
|
65
|
+
|
|
66
|
+
.totals { margin-left: auto; width: min(320px, 100%); }
|
|
67
|
+
.totals .kvrow dt { flex: 0 0 55%; text-align: right; }
|
|
68
|
+
.totals .kvrow dd { text-align: right; font-variant-numeric: tabular-nums; }
|
|
69
|
+
.totals .kvrow.grand { border-top: 2px solid #b4c3d2; font-weight: 700; font-size: 13px; }
|
|
70
|
+
|
|
71
|
+
.sublist { border: 1px solid var(--line); margin-bottom: 12px; }
|
|
72
|
+
.sublist > header {
|
|
73
|
+
display: flex; align-items: center; gap: 8px;
|
|
74
|
+
background: var(--band); border-bottom: 1px solid var(--line); padding: 4px 9px;
|
|
75
|
+
}
|
|
76
|
+
.sublist > header h3 { margin: 0; font-size: 11px; font-weight: 700; text-transform: uppercase; color: #2b3a49; }
|
|
77
|
+
.sublist > header .grow { margin-left: auto; }
|
|
78
|
+
|
|
79
|
+
.formgrid .field.inline { grid-column: 1 / -1; }
|
|
80
|
+
.formgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0 22px; }
|
|
81
|
+
.linegrid input, .linegrid select { min-width: 70px; }
|
|
82
|
+
.linegrid td { vertical-align: middle; }
|
|
83
|
+
|
|
84
|
+
/* ---- SuiteQL tool ---- */
|
|
85
|
+
.sql-editor { width: 100%; min-height: 130px; font-family: "SFMono-Regular", Menlo, Consolas, monospace; font-size: 12px; }
|
|
86
|
+
.sql-out { background: #11202f; color: #cfe3f5; padding: 10px; overflow: auto; max-height: 260px; font-size: 11px; }
|
|
87
|
+
.sql-out pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
|
|
88
|
+
|
|
89
|
+
@media (max-width: 1080px) { .dash { grid-template-columns: 1fr 1fr; } }
|
|
90
|
+
@media (max-width: 720px) {
|
|
91
|
+
.dash { grid-template-columns: 1fr; }
|
|
92
|
+
.record-head .rh-actions { margin-left: 0; }
|
|
93
|
+
.kvrow { flex-direction: column; gap: 0; }
|
|
94
|
+
.kvrow dt { flex: none; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ---- home portlets added for the full dashboard: navigation tree, trend graph, snapshots, custom search, calendar ---- */
|
|
98
|
+
.nav-root, .nav-tree { list-style: none; margin: 0; padding: 0; }
|
|
99
|
+
.nav-root > li { border-bottom: 1px solid #edf0f4; }
|
|
100
|
+
.nav-root > li:last-child { border-bottom: none; }
|
|
101
|
+
.nav-branch { display: flex; align-items: center; gap: 6px; width: 100%; padding: 4px 0; border: 0; background: none; font: inherit; font-weight: 700; color: #1b2a3a; cursor: pointer; text-align: left; }
|
|
102
|
+
.nav-branch svg { transition: transform .12s; color: #5a6b7c; }
|
|
103
|
+
.nav-branch[aria-expanded="false"] svg { transform: rotate(-90deg); }
|
|
104
|
+
.nav-tree { padding: 0 0 4px 22px; columns: 2; column-gap: 12px; }
|
|
105
|
+
.nav-tree li { break-inside: avoid; }
|
|
106
|
+
.nav-link { display: block; padding: 2px 0; border: 0; background: none; font: inherit; font-size: 12px; color: var(--link, #1a5fb4); cursor: pointer; text-align: left; }
|
|
107
|
+
.nav-link:hover { text-decoration: underline; }
|
|
108
|
+
.nav-link.off { color: #4b5763; }
|
|
109
|
+
.trend-head { display: flex; justify-content: space-between; align-items: baseline; font-size: 12px; margin-bottom: 2px; }
|
|
110
|
+
svg.trend { display: block; width: 100%; height: auto; }
|
|
111
|
+
svg.trend .axis { stroke: #b4c3d2; stroke-width: 1; }
|
|
112
|
+
svg.trend .bar { fill: #6b9bd1; }
|
|
113
|
+
svg.trend .bar.current { fill: #2e5f96; }
|
|
114
|
+
svg.trend .tick { font-size: 10px; fill: #4b5763; }
|
|
115
|
+
.snap-title { font-size: 12px; font-weight: 700; color: #1b2a3a; padding: 2px 0 4px; border-bottom: 1px solid #edf0f4; margin-bottom: 4px; }
|
|
116
|
+
.snap-bars { list-style: none; margin: 0; padding: 0; font-size: 12px; }
|
|
117
|
+
.snap-bars li { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(60px, 1fr) auto; align-items: center; gap: 8px; padding: 3px 0; border-bottom: 1px solid #edf0f4; }
|
|
118
|
+
.snap-bars li:last-child { border-bottom: none; }
|
|
119
|
+
.snap-bars a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
120
|
+
svg.snap-track { width: 100%; height: 8px; display: block; background: #edf0f4; }
|
|
121
|
+
svg.snap-track .snap-fill { fill: #6b9bd1; }
|
|
122
|
+
.snap-bars .num { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
123
|
+
table.search-results { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
124
|
+
table.search-results th { text-align: left; font-weight: 700; color: #4b5763; padding: 3px 4px; border-bottom: 1px solid #b4c3d2; background: #f3f6f9; white-space: nowrap; }
|
|
125
|
+
table.search-results td { padding: 3px 4px; border-bottom: 1px solid #edf0f4; vertical-align: top; }
|
|
126
|
+
table.search-results td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
127
|
+
table.search-results td.overdue { color: var(--red); font-weight: 700; }
|
|
128
|
+
.cal-head { display: flex; justify-content: space-between; align-items: baseline; font-size: 12px; margin-bottom: 4px; }
|
|
129
|
+
table.cal { width: 100%; border-collapse: collapse; font-size: 11px; text-align: center; table-layout: fixed; }
|
|
130
|
+
table.cal th { font-weight: 700; color: #4b5763; padding: 2px 0; border-bottom: 1px solid #b4c3d2; }
|
|
131
|
+
table.cal td { padding: 3px 0; border: 1px solid #edf0f4; height: 22px; }
|
|
132
|
+
table.cal td.pad { background: #f7f9fb; }
|
|
133
|
+
table.cal td.today { background: #2e5f96; color: #fff; font-weight: 700; }
|
|
134
|
+
.portlet .btn-row { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* NetSuite list views: the blue header row, banded rows, the paging strip and the quick-filter bar. */
|
|
2
|
+
.listview { background: var(--panel); border: 1px solid var(--line); }
|
|
3
|
+
|
|
4
|
+
.listhead {
|
|
5
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
6
|
+
background: linear-gradient(#f4f6f9, #e6eaf0);
|
|
7
|
+
border-bottom: 1px solid var(--line);
|
|
8
|
+
padding: 7px 10px;
|
|
9
|
+
}
|
|
10
|
+
.listhead h1 { margin: 0; font-size: 15px; font-weight: 700; color: #1b2a3a; }
|
|
11
|
+
.listhead .count { color: var(--ink-soft); font-size: 11px; }
|
|
12
|
+
.listhead .grow { margin-left: auto; }
|
|
13
|
+
|
|
14
|
+
.filterbar {
|
|
15
|
+
display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap;
|
|
16
|
+
background: var(--band-2);
|
|
17
|
+
border-bottom: 1px solid var(--line-soft);
|
|
18
|
+
padding: 7px 10px;
|
|
19
|
+
}
|
|
20
|
+
.filterbar .field { margin: 0; }
|
|
21
|
+
.filterbar input[type="search"], .filterbar input[type="text"], .filterbar select { min-width: 180px; width: auto; }
|
|
22
|
+
|
|
23
|
+
.tabstrip { display: flex; gap: 0; background: var(--band-2); border-bottom: 1px solid var(--line); padding: 0 8px; }
|
|
24
|
+
.tabstrip button {
|
|
25
|
+
background: none; border: none; border-bottom: 3px solid transparent;
|
|
26
|
+
font: inherit; font-size: 12px; color: #3d4957; padding: 6px 12px; cursor: pointer;
|
|
27
|
+
}
|
|
28
|
+
.tabstrip button:hover { color: var(--link); }
|
|
29
|
+
.tabstrip button[aria-selected="true"] { color: #12344f; font-weight: 700; border-bottom-color: #2c4f7c; }
|
|
30
|
+
|
|
31
|
+
.tablewrap { overflow-x: auto; }
|
|
32
|
+
table.grid { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
33
|
+
table.grid thead th {
|
|
34
|
+
background: linear-gradient(#5d84b0, #416a97);
|
|
35
|
+
color: #fff;
|
|
36
|
+
text-align: left;
|
|
37
|
+
font-weight: 700;
|
|
38
|
+
font-size: 11px;
|
|
39
|
+
padding: 5px 8px;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
border-right: 1px solid #38618c;
|
|
42
|
+
position: sticky;
|
|
43
|
+
top: 0;
|
|
44
|
+
}
|
|
45
|
+
table.grid thead th:last-child { border-right: none; }
|
|
46
|
+
table.grid thead th.num, table.grid td.num { text-align: right; }
|
|
47
|
+
table.grid tbody td {
|
|
48
|
+
padding: 4px 8px;
|
|
49
|
+
border-bottom: 1px solid #e3e7ec;
|
|
50
|
+
vertical-align: top;
|
|
51
|
+
}
|
|
52
|
+
table.grid tbody tr:nth-child(even) { background: #f6f8fa; }
|
|
53
|
+
table.grid tbody tr:hover { background: #fff6d9; }
|
|
54
|
+
table.grid tbody tr.selected { background: #dcebfa; }
|
|
55
|
+
table.grid td.tight { white-space: nowrap; }
|
|
56
|
+
table.grid td .rowlink { font-weight: 600; }
|
|
57
|
+
|
|
58
|
+
.pager {
|
|
59
|
+
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
60
|
+
background: var(--band-2);
|
|
61
|
+
border-top: 1px solid var(--line-soft);
|
|
62
|
+
padding: 6px 10px;
|
|
63
|
+
font-size: 11px;
|
|
64
|
+
color: #3d4957;
|
|
65
|
+
}
|
|
66
|
+
.pager .grow { margin-left: auto; }
|
|
67
|
+
.pager select { width: auto; min-width: 64px; }
|
|
68
|
+
|
|
69
|
+
.empty { padding: 26px 14px; text-align: center; color: var(--ink-soft); }
|
|
70
|
+
.empty strong { display: block; color: var(--ink); font-size: 13px; margin-bottom: 3px; }
|
|
71
|
+
|
|
72
|
+
.skeleton { height: 11px; background: linear-gradient(90deg, #e9edf2, #f5f7fa, #e9edf2); border-radius: 2px; }
|
|
73
|
+
.sk-row td { padding: 6px 8px; }
|
|
74
|
+
|
|
75
|
+
.banner { display: flex; gap: 8px; padding: 9px 11px; border: 1px solid; margin-bottom: 10px; align-items: flex-start; }
|
|
76
|
+
.banner.error { background: #fdecea; border-color: #dda9a5; color: #7d1a17; }
|
|
77
|
+
.banner.warn { background: #fdf5e2; border-color: #ddc78d; color: #6d4c06; }
|
|
78
|
+
.banner.info { background: #eaf2fb; border-color: #a9c4e2; color: #1b4269; }
|
|
79
|
+
.banner-title { font-weight: 700; }
|
|
80
|
+
.banner-detail { margin-top: 2px; }
|
|
81
|
+
|
|
82
|
+
.pill {
|
|
83
|
+
display: inline-block; padding: 0 7px; border-radius: 9px; font-size: 10px; font-weight: 700;
|
|
84
|
+
border: 1px solid; white-space: nowrap; line-height: 16px;
|
|
85
|
+
}
|
|
86
|
+
.pill.open { background: #e8f1fb; border-color: #a6c5e6; color: #14538e; }
|
|
87
|
+
.pill.paid { background: #e8f5eb; border-color: #a3ceb1; color: var(--green); }
|
|
88
|
+
.pill.pending { background: #fdf4e3; border-color: #ddc78d; color: var(--amber); }
|
|
89
|
+
.pill.billing { background: #e4f3f4; border-color: #9dc9cd; color: var(--teal); }
|
|
90
|
+
.pill.closed { background: #eceff2; border-color: #c3cbd6; color: var(--grey); }
|
|
91
|
+
.pill.void { background: #eceff2; border-color: #c3cbd6; color: var(--grey); text-decoration: line-through; }
|
|
92
|
+
.overdue { color: var(--red); font-weight: 700; }
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Safe DOM helpers, operation calls, NetSuite formatting and banners. Record text only goes through textContent.
|
|
2
|
+
import { invoke } from "/_firedrill/client.js";
|
|
3
|
+
import { icon } from "./icons.js";
|
|
4
|
+
|
|
5
|
+
export const $ = (sel, root = document) => root.querySelector(sel);
|
|
6
|
+
export function el(tag, attrs = {}, children = []) {
|
|
7
|
+
const node = document.createElement(tag);
|
|
8
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
9
|
+
if (v === undefined || v === null || v === false) continue;
|
|
10
|
+
if (k === "class") node.className = v;
|
|
11
|
+
else if (k === "text") node.textContent = String(v);
|
|
12
|
+
else if (k.startsWith("on") && typeof v === "function") node.addEventListener(k.slice(2), v);
|
|
13
|
+
else if (k === "value") node.value = v;
|
|
14
|
+
else node.setAttribute(k, v === true ? "" : String(v));
|
|
15
|
+
}
|
|
16
|
+
for (const child of [].concat(children)) {
|
|
17
|
+
if (child === null || child === undefined || child === false) continue;
|
|
18
|
+
node.append(child instanceof Node ? child : document.createTextNode(String(child)));
|
|
19
|
+
}
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
22
|
+
export { icon };
|
|
23
|
+
|
|
24
|
+
export class ToolError extends Error {
|
|
25
|
+
constructor(status, code, message, details) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.status = status;
|
|
28
|
+
this.code = String(code ?? status);
|
|
29
|
+
this.details = details ?? {};
|
|
30
|
+
}
|
|
31
|
+
get denied() { return this.status === "denied"; }
|
|
32
|
+
is(code) { return this.code === code || this.code.endsWith(`.${code}`); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let pending = 0;
|
|
36
|
+
export const isPending = () => pending > 0;
|
|
37
|
+
export async function call(operationId, args = {}, idempotencyKey) {
|
|
38
|
+
pending += 1;
|
|
39
|
+
try {
|
|
40
|
+
const result = await invoke(operationId, args, idempotencyKey ? { idempotencyKey } : {});
|
|
41
|
+
if (result.outcome.status !== "ok") {
|
|
42
|
+
const error = result.outcome.error ?? {};
|
|
43
|
+
throw new ToolError(
|
|
44
|
+
result.outcome.status,
|
|
45
|
+
error.code ?? result.outcome.status,
|
|
46
|
+
error.message ?? `The request was ${result.outcome.status}.`,
|
|
47
|
+
error.details,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return result.outcome.value;
|
|
51
|
+
} finally {
|
|
52
|
+
pending -= 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const newKey = () => crypto.randomUUID();
|
|
56
|
+
|
|
57
|
+
export function isDenied(error) {
|
|
58
|
+
return error instanceof ToolError && (error.denied || error.is("INSUFFICIENT_PERMISSION") || error.is("INVALID_LOGIN"));
|
|
59
|
+
}
|
|
60
|
+
export function describe(error) {
|
|
61
|
+
if (!(error instanceof ToolError)) return error?.message ?? "Something went wrong. Try again.";
|
|
62
|
+
if (error.denied) return "This Firedrill actor is not granted this NetSuite operation.";
|
|
63
|
+
if (error.is("CONCURRENCY_LIMIT_EXCEEDED")) return "Request limit exceeded. Wait a moment and try again.";
|
|
64
|
+
if (error.is("RESULT_SET_TOO_LARGE")) return `${error.message} Narrow the filter and try again.`;
|
|
65
|
+
return error.message;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- formatting (calendar values come from world time through session.get, never the browser clock) ----
|
|
69
|
+
const MONEY = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
70
|
+
export const money = (value, currency = "") => `${currency ? `${currency} ` : ""}${MONEY.format(Number(value) || 0)}`;
|
|
71
|
+
export const qty = (value) => new Intl.NumberFormat("en-US", { maximumFractionDigits: 4 }).format(Number(value) || 0);
|
|
72
|
+
/** "2026-05-12" → "5/12/2026", the account's M/D/YYYY date format. */
|
|
73
|
+
export function mdy(date) {
|
|
74
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(date ?? ""));
|
|
75
|
+
return m ? `${Number(m[2])}/${Number(m[3])}/${m[1]}` : "";
|
|
76
|
+
}
|
|
77
|
+
export function dayNum(date) {
|
|
78
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(date ?? ""));
|
|
79
|
+
return m ? Math.floor(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) / 86400000) : NaN;
|
|
80
|
+
}
|
|
81
|
+
export const addDays = (date, days) => {
|
|
82
|
+
const n = dayNum(date);
|
|
83
|
+
return Number.isNaN(n) ? date : new Date((n + days) * 86400000).toISOString().slice(0, 10);
|
|
84
|
+
};
|
|
85
|
+
export const dateOnly = (value) => String(value ?? "").slice(0, 10);
|
|
86
|
+
/** Quote a value for the NetSuite `q` record-filter grammar. */
|
|
87
|
+
export const lit = (text) => `"${String(text).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
88
|
+
|
|
89
|
+
// ---- banners, toasts, skeletons ----
|
|
90
|
+
let toastTimer;
|
|
91
|
+
export function toast(message, { error = false } = {}) {
|
|
92
|
+
const host = $("#toast");
|
|
93
|
+
host.replaceChildren(icon(error ? "alert" : "check", 16), el("span", { text: message }));
|
|
94
|
+
host.className = `toast ${error ? "error" : "success"}`;
|
|
95
|
+
host.hidden = false;
|
|
96
|
+
clearTimeout(toastTimer);
|
|
97
|
+
toastTimer = setTimeout(() => { host.hidden = true; }, error ? 9000 : 4000);
|
|
98
|
+
}
|
|
99
|
+
export function banner(kind, title, detail, extra) {
|
|
100
|
+
return el("div", { class: `banner ${kind}`, role: kind === "error" ? "alert" : "status" }, [
|
|
101
|
+
icon(kind === "info" ? "info" : kind === "warn" ? "lock" : "alert", 17),
|
|
102
|
+
el("div", {}, [
|
|
103
|
+
el("div", { class: "banner-title", text: title }),
|
|
104
|
+
detail ? el("div", { class: "banner-detail", text: detail }) : null,
|
|
105
|
+
extra ?? null,
|
|
106
|
+
]),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
export function errorBanner(error, title = "This request could not be completed") {
|
|
110
|
+
if (isDenied(error)) return banner("warn", "Permission Violation", describe(error));
|
|
111
|
+
return banner("error", title, describe(error));
|
|
112
|
+
}
|
|
113
|
+
export function emptyState(title, detail) {
|
|
114
|
+
return el("div", { class: "empty" }, [el("strong", { text: title }), detail ? el("div", { text: detail }) : null]);
|
|
115
|
+
}
|
|
116
|
+
export function skeletonRows(columns, rows = 8) {
|
|
117
|
+
return Array.from({ length: rows }, () =>
|
|
118
|
+
el("tr", { class: "sk-row" }, Array.from({ length: columns }, () => el("td", {}, el("div", { class: "skeleton" })))));
|
|
119
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Page scaffolding shared by every screen: list views, the NetSuite paging strip, record headers,
|
|
2
|
+
// field groups and the standard record button row.
|
|
3
|
+
import { $, el, icon, money } from "../ui.js";
|
|
4
|
+
import { notSimulated } from "../overlay.js";
|
|
5
|
+
import { app, statusClass } from "../store.js";
|
|
6
|
+
|
|
7
|
+
export function pill(status) {
|
|
8
|
+
const name = status?.refName ?? status ?? "";
|
|
9
|
+
return el("span", { class: `pill ${statusClass(name)}`, text: name });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function link(text, href, extra = {}) {
|
|
13
|
+
return el("a", { href, text, ...extra });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toolbarButton(label, run, { kind = "", disabled = false, title } = {}) {
|
|
17
|
+
return el("button", { type: "button", class: `btn ${kind}`, text: label, disabled, title, onclick: run });
|
|
18
|
+
}
|
|
19
|
+
/** A control the real product shows here but this Tool does not implement. */
|
|
20
|
+
export function nsButton(label, { kind = "" } = {}) {
|
|
21
|
+
return el("button", { type: "button", class: `btn ${kind}`, text: label, title: "Not simulated by this Tool", onclick: () => notSimulated(label) });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The NetSuite list-view frame: title row, optional filter bar, a grid body and the paging strip. */
|
|
25
|
+
export function listShell({ title, subtitle, actions = [], filter, tabs, columns, body, pager }) {
|
|
26
|
+
const head = el("header", { class: "listhead" }, [
|
|
27
|
+
el("h1", { text: title }),
|
|
28
|
+
subtitle ? el("span", { class: "count", text: subtitle }) : null,
|
|
29
|
+
el("span", { class: "grow" }),
|
|
30
|
+
...actions,
|
|
31
|
+
]);
|
|
32
|
+
const table = el("table", { class: "grid" }, [
|
|
33
|
+
el("thead", {}, el("tr", {}, columns.map((column) =>
|
|
34
|
+
el("th", { class: column.numeric ? "num" : "", scope: "col", text: column.label })))),
|
|
35
|
+
body,
|
|
36
|
+
]);
|
|
37
|
+
return el("section", { class: "listview" }, [
|
|
38
|
+
head,
|
|
39
|
+
tabs ?? null,
|
|
40
|
+
filter ?? null,
|
|
41
|
+
el("div", { class: "tablewrap" }, table),
|
|
42
|
+
pager ?? null,
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** NetSuite's "1 to 25 of 137" strip with page size, previous and next. */
|
|
47
|
+
export function pagerStrip({ offset, shown, total, hasMore, pageSize, onOffset, onPageSize }) {
|
|
48
|
+
const from = total === 0 ? 0 : offset + 1;
|
|
49
|
+
const to = offset + shown;
|
|
50
|
+
const sizeId = "page-size";
|
|
51
|
+
return el("div", { class: "pager" }, [
|
|
52
|
+
el("label", { class: "fl", for: sizeId, text: "Rows per page" }),
|
|
53
|
+
el("select", {
|
|
54
|
+
id: sizeId,
|
|
55
|
+
onchange: (event) => onPageSize(Number(event.currentTarget.value)),
|
|
56
|
+
}, [10, 25, 50].map((size) => el("option", { value: String(size), text: String(size), selected: size === pageSize }))),
|
|
57
|
+
el("span", { class: "grow" }),
|
|
58
|
+
el("span", { text: `${from} to ${to} of ${total}` }),
|
|
59
|
+
el("button", {
|
|
60
|
+
type: "button", class: "btn", disabled: offset === 0, "aria-label": "Previous page",
|
|
61
|
+
onclick: () => onOffset(Math.max(0, offset - pageSize)),
|
|
62
|
+
}, [icon("chevronLeft", 14)]),
|
|
63
|
+
el("button", {
|
|
64
|
+
type: "button", class: "btn", disabled: !hasMore, "aria-label": "Next page",
|
|
65
|
+
onclick: () => onOffset(offset + pageSize),
|
|
66
|
+
}, [icon("chevronRight", 14)]),
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Record page frame: the pale header band, the standard button row and a subtab strip. */
|
|
71
|
+
export function recordShell({ title, subtitle, actions = [], subtabs, body }) {
|
|
72
|
+
return el("section", { class: "record" }, [
|
|
73
|
+
el("header", { class: "record-head" }, [
|
|
74
|
+
el("div", { class: "rh-title" }, [
|
|
75
|
+
el("h1", { text: title }),
|
|
76
|
+
subtitle ? el("span", { class: "rh-sub", text: subtitle }) : null,
|
|
77
|
+
]),
|
|
78
|
+
el("div", { class: "rh-actions" }, actions),
|
|
79
|
+
]),
|
|
80
|
+
subtabs ?? null,
|
|
81
|
+
el("div", { class: "record-body" }, body),
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The Edit / Back / Actions / Print / Email / More row every NetSuite record page carries. */
|
|
86
|
+
export function recordButtons({ onEdit, onBack, actions = [], editLabel = "Edit", editDisabled = false } = {}) {
|
|
87
|
+
const row = [];
|
|
88
|
+
if (onEdit) row.push(toolbarButton(editLabel, onEdit, { kind: "primary", disabled: editDisabled }));
|
|
89
|
+
if (onBack) row.push(toolbarButton("Back", onBack));
|
|
90
|
+
row.push(...actions, nsButton("Print"), nsButton("Email"), nsButton("More"));
|
|
91
|
+
return row;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function subtabStrip(names, current, onSelect) {
|
|
95
|
+
return el("div", { class: "subtabs", role: "tablist" }, names.map((name) =>
|
|
96
|
+
el("button", {
|
|
97
|
+
type: "button", role: "tab", "aria-selected": String(name === current), text: name,
|
|
98
|
+
onclick: () => onSelect(name),
|
|
99
|
+
})));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function fieldGroup(title, children) {
|
|
103
|
+
return el("section", { class: "fieldgroup" }, [
|
|
104
|
+
el("h3", { text: title }),
|
|
105
|
+
el("div", { class: "fg-body" }, children),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function kv(label, value) {
|
|
110
|
+
return el("div", { class: "kvrow" }, [
|
|
111
|
+
el("dt", { text: label }),
|
|
112
|
+
el("dd", {}, value instanceof Node ? value : el("span", { text: value === undefined || value === null || value === "" ? "" : String(value) })),
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function kvColumns(...groups) {
|
|
117
|
+
return el("div", { class: "fieldcols" }, groups.map((rows) => el("dl", { class: "kvcol" }, rows)));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function totalsBlock(rows) {
|
|
121
|
+
return el("dl", { class: "totals" }, rows.map(([label, value, grand]) =>
|
|
122
|
+
el("div", { class: `kvrow${grand ? " grand" : ""}` }, [el("dt", { text: label }), el("dd", { text: value })])));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const amountCell = (value, currency) => el("td", { class: "num", text: money(value, currency) });
|
|
126
|
+
|
|
127
|
+
/** Currency code of a record, falling back to the account's base currency. */
|
|
128
|
+
export const currencyOf = (record) => record?.currency?.refName ?? app.session?.account?.baseCurrency ?? "";
|
|
129
|
+
|
|
130
|
+
export function setPageTitle(text) {
|
|
131
|
+
document.title = `${text} · NetSuite (synthetic)`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const host = () => $("#main");
|