feat(est-test): 기성 환경설정(공통·기성고·원가계산 칸, 적용·취소, Ctrl+U)

- 기성고 화면을 native-progress-view.js 로 옮김
- 코드·레벨·사정금액·노재경 구분(비목순서·실적)·비고란·집계공종 진하게·수량/금액 0 숨김·품명정렬·두줄·바탕색 반영
- Enter 뒤 셀 이동 방향, 취소면 연 때 값으로 되돌림
- 주 메뉴 환경설정은 기성 화면에서 이 창을 엶

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 19:03:36 +09:00
co-authored by Claude Opus 5.5
parent 7b765c2599
commit 812928a51f
9 changed files with 367 additions and 103 deletions
+1
View File
@@ -35,6 +35,7 @@ for (const file of [
"native-block-actions.js",
"native-extra.js",
"native-applied-cost.js",
"native-progress-view.js",
"native-business-actions.js",
"native-forms.js",
"native-excel-to-est.js",
+61
View File
@@ -0,0 +1,61 @@
"""기성 환경설정: 옵션 기본값·비목 열 순서·표 머리 반영."""
from test_est_rea_parity import run
def test_defaults_and_part_order():
result = run(
"math",
"""(()=>{
const d=EstProgressView.get({});
const o=(x)=>EstProgressView.parts(EstProgressView.get({Display:{order:x.order},AccomOption:x}));
return {code:d.code,sajung:d.sajung,gubun:d.gubun,
a:o({order:0}),b:o({order:2,siljuk:true}),c:o({order:3}),d:o({order:5})};
})()""",
)
assert result == {
"code": True,
"sajung": True,
"gubun": False,
"a": ["T", "N", "G", "J"],
"b": ["N", "G", "J", "S", "T"],
"c": ["G", "N", "J", "T"],
"d": ["T", "N", "G", "J"],
}
def test_view_headers_follow_options_and_zero_hiding():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;
p.tables.EstMainDat=[{Code:'A',Name:'a',GN:10,GU:100},{LinkIndex:54}];
const q=EstNative.calculate(p);q.progress=[{id:'r1',name:'1회',inputs:{},locked:false}];
const g=globalThis;g.EstApp={...(g.EstApp||{}),esc:String,number:String};
g.EstProgressWonga={render:()=>''};
const box=(t,tools,body)=>body,button=()=>'',table=(h,rows)=>JSON.stringify({h,rows});
const st={progressPage:0,selection:new Set()};
const head=(o)=>{q.options.Display=o.d;q.options.AccomOption=o.a;
return JSON.parse(EstProgressView.view({...st},q,{box,button,table}).split('<p')[0]);};
const plain=head({});
const custom=head({d:{code:false,gnZero:false},a:{level:true,sajung:false,gubun:true,gungu:true}});
return {plain:plain.h,custom:custom.h,jun:custom.rows[0][3]};
})()""",
)
assert result["plain"][0] == "코드" and "사정금액" in result["plain"]
assert result["custom"] == [
"레벨",
"공종",
"도급수량",
"전회수량",
"금회 입력",
"입력방법",
"금회금액",
"금회 노무",
"금회 재료",
"금회 경비",
"누계수량",
"누계금액",
"비고",
]
assert result["jun"] == ""
+1
View File
@@ -22,6 +22,7 @@
<script defer src="native-excel-to-est.js"></script>
<script defer src="native-other-load.js"></script>
<script defer src="native-applied-cost.js"></script>
<script defer src="native-progress-view.js"></script>
<script defer src="native-actions-more.js"></script>
<script defer src="native-wonga.js"></script>
<script defer src="native-jungsang.js"></script>
+1
View File
@@ -8,6 +8,7 @@ const EstActionsMore = (() => {
typeof EstExcelToEst !== "undefined" && EstExcelToEst,
typeof EstOtherLoad !== "undefined" && EstOtherLoad,
typeof EstAppliedCost !== "undefined" && EstAppliedCost,
typeof EstProgressView !== "undefined" && EstProgressView,
].filter(Boolean);
function handle(name, state, node) {
for (const module of modules()) if (module.handle(name, state, node)) return true;
+2 -1
View File
@@ -333,7 +333,8 @@ const EstActions = (() => {
"REA 샘플 대조",
`<p><b>${result.checked}개 중 ${result.passed}개 일치 · 불일치 ${result.failed}개</b></p><p>보관된 EST 샘플의 저장값과 계산 결과를 비교했습니다. 원본 실행 비교는 포함하지 않습니다.</p>${result.errors.length ? `<p>${esc(result.errors.join(" / "))}</p>` : ""}<table><thead><tr><th>영역</th><th>검사 수</th><th>일치</th></tr></thead><tbody>${[...new Set(result.results.map((r) => r.group))].map((group) => `<tr><td>${esc(group)}</td><td>${result.results.filter((r) => r.group === group).length}</td><td>${result.results.filter((r) => r.group === group && r.pass).length}</td></tr>`).join("")}</tbody></table>`,
);
} else if (name === "settings") EstSettings.show();
} else if (name === "settings")
EstApp.state.view === "progress" ? EstProgressView.dialog(EstApp.state) : EstSettings.show();
else if (name === "raw-line") {
const row = activeRows()[Number(node.dataset.row)];
showDialog(
+1 -102
View File
@@ -126,108 +126,7 @@ const EstExtra = (() => {
: "현재 작업을 저장한 뒤 당초 내역을 확정하세요.",
);
} else if (view === "progress") {
state.roundId ||= project.progress[0]?.id;
const round = project.progress.find((r) => r.id === state.roundId);
let body = '<p class="empty">회차를 추가하세요.</p>',
tools =
button("add-round", "기성 회차 추가") +
'<label>원본 기성 표 확인 <input type="file" accept=".estx" data-accom-inspect></label>' +
(project.progress.length ? button("clear-progress", "기성 전체 초기화") : "");
if (round) {
tools +=
`<select id="round-choice">${project.progress.map((r) => `<option value="${esc(r.id)}" ${r.id === round.id ? "selected" : ""}>${esc(r.name)}${r.locked ? " (확정)" : ""}</option>`).join("")}</select>` +
button(
round.locked ? "unlock-round" : "lock-round",
round.locked ? "확정 해제(이후 포함)" : "회차 확정·저장",
) +
button("fill-progress", "전량·비율 일괄 입력") +
button("rename-round", "회차 이름") +
button("delete-round", "회차 삭제") +
button("reset-round-inputs", "회차 입력 초기화") +
`<label><input id="progress-components" type="checkbox" ${round.separateComponents !== false ? "checked" : ""} ${round.locked ? "disabled" : ""}> 비목별 계산</label><label>금액 소수자리 <input id="progress-digits" type="number" min="0" max="4" value="${round.amountDigits ?? 0}" ${round.locked ? "disabled" : ""}></label>` +
`<label><input id="use-approved" type="checkbox" ${round.approved ? "checked" : ""} ${round.locked ? "disabled" : ""}> 누계에 사정 사용</label>`;
let output;
try {
output =
round.locked && round.result ? round.result : EstBusiness.progress(project, round.id);
} catch (e) {
output = { rows: [], errors: [e.message] };
}
state.business = output;
const page = EstBlockActions.progressPage(state, output.rows);
tools +=
button("progress-prev", "이전", state.progressPage === 0 ? "disabled" : "") +
`<span>${state.progressPage + 1} / ${page.pages}쪽 · ${page.total}행</span>` +
button("progress-next", "다음", state.progressPage + 1 >= page.pages ? "disabled" : "");
body =
table(
[
"코드",
"공종",
"도급수량",
"전회수량",
"금회 입력",
"입력방법",
"사정 입력",
"입력방법",
"금회금액",
"사정금액",
"누계수량",
"누계금액",
],
page.rows.map(({ row, index }) => {
const editable = ![54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex));
const key = EstBusiness.rowKey(row, index),
input = round.inputs?.[key] || {};
const edit = (prefix) =>
`<input data-progress="${prefix}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix] ?? row[prefix + "GN"] ?? 0}" ${round.locked || !editable || input[prefix + "Mode"] === "components" ? "disabled" : ""}>` +
(input[prefix + "Mode"] === "components"
? EstMath.components
.map(
(field, i) =>
`<label>${["노무", "재료", "경비", "실적"][i]}<input data-progress="${prefix + field}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix + field] ?? 0}" ${round.locked ? "disabled" : ""}></label>`,
)
.join("")
: "");
const method = (prefix) =>
!editable
? "자동계산"
: `<select data-progress="${prefix}Mode" data-key="${esc(key)}" ${round.locked ? "disabled" : ""}>${[
["quantity", "수량"],
["amount", "금액"],
["ratio", "비율 %"],
["components", "비목 금액"],
]
.map(
([v, label]) =>
`<option value="${v}" ${v === (input[`${prefix}Mode`] || "quantity") ? "selected" : ""}>${label}</option>`,
)
.join("")}</select>`;
return [
esc(row.Code),
esc(row.Name),
number(row.GN),
number(row.JunGN),
edit("Gum"),
method("Gum"),
edit("Sa"),
method("Sa"),
number(row.GumTC),
number(row.SaTC),
number(row.TotalGN),
number(row.TotalTC),
];
}),
) +
`<p class="business-note">${esc((output.errors || []).join(" / "))} 금회 도급액 ${number(output.total)} · 기성비율 ${number(output.ratio)}%</p>`;
body += EstProgressWonga.render(state, round, output);
}
html = box(
"기성고",
tools,
body,
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
);
html = EstProgressView.view(state, project, { box, button, table });
} else if (view === "escalation") {
html = EstAppliedCost.view(state, project, { box, button, table });
} else if (view === "annual") {
+298
View File
@@ -0,0 +1,298 @@
// 기성고 화면(native-extra.js 에서 옮김)과 원본 기성 「환경설정」(TfrmAccomOption — spec_pb1 §4: 주 메뉴 환경 설정 0x10ccddc 가
// 기성 문서일 때 · 기성 원가계산서 팝업 popupOptision 0xd479f4 Ctrl+U). 칸을 누르면 바로 옵션에 쓰고 「취소」면 연 때 값으로 되돌림(d45e1c).
// 공통적용(코드·수량 0·금액 0·비목순서·품명정렬·품명 두줄·Enter 이동·바탕색)은 다른 표와 같은 계산 환경 Display,
// 기성고(레벨·비고란 발췌근거·집계공종 진하게·사정금액·노재경 구분·실적 비목)는 AccomOption. 원본도 처리기가 없는
// 자동저장 분·가로세로 계산 확인·가로 계산방법·재료비 처리·이윤 사사오입은 값만 담는다(화면·계산 불변).
const EstProgressView = (() => {
const num = (v) => Number(v) || 0;
const COMMON = {
code: true,
gnZero: true,
costZero: true,
order: 0,
align: 0,
twoLine: false,
enter: 0,
color: "",
autoSave: false,
saveTime: 10,
};
const ACCOM = {
level: false,
gungu: false,
sumBold: false,
calcCheck: false,
horCalc: 0,
sajung: true,
gubun: false,
siljuk: false,
gcTo: 0,
eyunRound: false,
};
const get = (options) => ({
...COMMON,
...(options?.Display || {}),
...ACCOM,
...(options?.AccomOption || {}),
});
const ORDERS = ["TNGJ", "TGNJ", "NGJT", "GNJT"];
const PART = { T: "합계", N: "노무", G: "재료", J: "경비", S: "실적" };
// 비목 금액 열(금회): 비목순서대로, 실적은 실적 비목 표시 때만 경비 뒤에
function parts(o) {
const seq = [...(ORDERS[o.order] || ORDERS[0])];
if (o.siljuk) seq.splice(seq.indexOf("J") + 1, 0, "S");
return seq;
}
function view(state, project, { box, button, table }) {
const { esc } = EstApp,
o = get(project.options);
const shown = (v, zero) => (!zero && !num(v) ? "" : EstApp.number(v));
const qty = (v) => shown(v, o.gnZero),
cost = (v) => shown(v, o.costZero);
state.roundId ||= project.progress[0]?.id;
const round = project.progress.find((r) => r.id === state.roundId);
let body = '<p class="empty">회차를 추가하세요.</p>',
tools =
button("add-round", "기성 회차 추가") +
'<label>원본 기성 표 확인 <input type="file" accept=".estx" data-accom-inspect></label>' +
(project.progress.length ? button("clear-progress", "기성 전체 초기화") : "") +
button("accom-option", "환경 설정");
if (round) {
tools +=
`<select id="round-choice">${project.progress.map((r) => `<option value="${esc(r.id)}" ${r.id === round.id ? "selected" : ""}>${esc(r.name)}${r.locked ? " (확정)" : ""}</option>`).join("")}</select>` +
button(
round.locked ? "unlock-round" : "lock-round",
round.locked ? "확정 해제(이후 포함)" : "회차 확정·저장",
) +
button("fill-progress", "전량·비율 일괄 입력") +
button("rename-round", "회차 이름") +
button("delete-round", "회차 삭제") +
button("reset-round-inputs", "회차 입력 초기화") +
`<label><input id="progress-components" type="checkbox" ${round.separateComponents !== false ? "checked" : ""} ${round.locked ? "disabled" : ""}> 비목별 계산</label><label>금액 소수자리 <input id="progress-digits" type="number" min="0" max="4" value="${round.amountDigits ?? 0}" ${round.locked ? "disabled" : ""}></label>` +
`<label><input id="use-approved" type="checkbox" ${round.approved ? "checked" : ""} ${round.locked ? "disabled" : ""}> 누계에 사정 사용</label>`;
let output;
try {
output =
round.locked && round.result ? round.result : EstBusiness.progress(project, round.id);
} catch (e) {
output = { rows: [], errors: [e.message] };
}
state.business = output;
const page = EstBlockActions.progressPage(state, output.rows);
tools +=
button("progress-prev", "이전", state.progressPage === 0 ? "disabled" : "") +
`<span>${state.progressPage + 1} / ${page.pages}쪽 · ${page.total}행</span>` +
button("progress-next", "다음", state.progressPage + 1 >= page.pages ? "disabled" : "");
const seq = o.gubun ? parts(o) : ["T"];
const nameStyle = [
["", "text-align:right", "text-align:center"][o.align] || "",
o.twoLine ? "white-space:normal;max-width:16em" : "white-space:nowrap",
]
.filter(Boolean)
.join(";");
const headers = [
...(o.code ? ["코드"] : []),
...(o.level ? ["레벨"] : []),
"공종",
"도급수량",
"전회수량",
"금회 입력",
"입력방법",
...(o.sajung ? ["사정 입력", "입력방법"] : []),
...seq.map((p) => (p === "T" ? "금회금액" : `금회 ${PART[p]}`)),
...(o.sajung ? ["사정금액"] : []),
"누계수량",
"누계금액",
...(o.gungu ? ["비고"] : []),
];
body =
table(
headers,
page.rows.map(({ row, index }) => {
const editable = ![54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex));
const key = EstBusiness.rowKey(row, index),
input = round.inputs?.[key] || {};
const edit = (prefix) =>
`<input data-progress="${prefix}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix] ?? row[prefix + "GN"] ?? 0}" ${round.locked || !editable || input[prefix + "Mode"] === "components" ? "disabled" : ""}>` +
(input[prefix + "Mode"] === "components"
? EstMath.components
.map(
(field, i) =>
`<label>${["노무", "재료", "경비", "실적"][i]}<input data-progress="${prefix + field}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix + field] ?? 0}" ${round.locked ? "disabled" : ""}></label>`,
)
.join("")
: "");
const method = (prefix) =>
!editable
? "자동계산"
: `<select data-progress="${prefix}Mode" data-key="${esc(key)}" ${round.locked ? "disabled" : ""}>${[
["quantity", "수량"],
["amount", "금액"],
["ratio", "비율 %"],
["components", "비목 금액"],
]
.map(
([v, label]) =>
`<option value="${v}" ${v === (input[`${prefix}Mode`] || "quantity") ? "selected" : ""}>${label}</option>`,
)
.join("")}</select>`;
const bold = o.sumBold && num(row.Level) > 0 ? ";font-weight:bold" : "";
return [
...(o.code ? [esc(row.Code)] : []),
...(o.level ? [num(row.Level) || ""] : []),
`<span style="display:block;${nameStyle}${bold}">${esc(row.Name)}</span>`,
qty(row.GN),
qty(row.JunGN),
edit("Gum"),
method("Gum"),
...(o.sajung ? [edit("Sa"), method("Sa")] : []),
...seq.map((p) => cost(row[`Gum${p}C`])),
...(o.sajung ? [cost(row.SaTC)] : []),
qty(row.TotalGN),
cost(row.TotalTC),
...(o.gungu ? [esc(row.Bigo ?? "")] : []),
];
}),
) +
`<p class="business-note">${esc((output.errors || []).join(" / "))} 금회 도급액 ${EstApp.number(output.total)} · 기성비율 ${EstApp.number(output.ratio)}%</p>`;
body += EstProgressWonga.render(state, round, output);
}
const html = box(
"기성고",
tools,
body,
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
);
return o.color ? `<div style="background:${esc(o.color)}">${html}</div>` : html;
}
// 환경설정 창: 칸을 바꾸면 바로 옵션에 쓰고(화면에 즉시), 「취소」 는 연 때 값으로 되돌림
let opened = null;
function dialog(state) {
const { esc } = EstApp,
o = get(state.project.options);
opened = {
Display: structuredClone(state.project.options.Display),
AccomOption: structuredClone(state.project.options.AccomOption),
};
const check = (name, label, extra = "") =>
`<label><input type="checkbox" data-ao="${name}" ${o[name] ? "checked" : ""} ${extra}> ${esc(label)}</label>`;
const pick = (name, label, choices, extra = "") =>
`<label>${esc(label)} <select data-ao="${name}" ${extra}>${choices.map((c, i) => `<option value="${i}" ${num(o[name]) === i ? "selected" : ""}>${esc(c)}</option>`).join("")}</select></label>`;
EstApp.showDialog(
"환경설정",
`<h4>공통적용</h4><div class="form-fields">${check("code", "코드표시")}${check("gnZero", '수량 "0" 표시')}${check("costZero", '금액 "0" 표시')}${check("autoSave", "데이타 자동저장")}<label>데이타 입력이 없으면 <input type="number" min="5" max="30" step="5" data-ao="saveTime" value="${num(o.saveTime)}" ${o.autoSave ? "" : "disabled"}> 분후 자동저장</label>${pick("order", "비목순서", ["합/노/재/경", "합/재/노/경", "노/재/경/합", "재/노/경/합"], o.gubun ? "" : "disabled")}${pick("align", "품명정렬", ["좌정렬", "우정렬", "중앙정렬"])}${pick("twoLine", "품명표기", ["한줄로 표시", "두줄로 표시"])}${pick("enter", "Enter 뒤 셀 이동", ["이동안함", "좌", "우", "위", "아래"])}<label>바탕색 <input type="color" data-ao="color" value="${esc(o.color || "#ffffff")}"></label></div>` +
`<h4>기성고</h4><div class="form-fields">${check("level", "레벨 표시")}${check("gungu", "비고란에 발췌근거 표시")}${check("sumBold", "집계공종 진하게 표시")}${check("calcCheck", "가로,세로 계산 확인")}${pick("horCalc", "데이타(가로) 계산방법", ["절하", "절상"])}${check("sajung", "사정금액 표시")}${check("gubun", "노,재,경 구분 표시")}${check("siljuk", "실적 비목 표시", o.gubun ? "" : "disabled")}</div>` +
`<h4>원가계산</h4><div class="form-fields">${pick("gcTo", "순직접공사비의 재료비 처리", ["간접재료비", "직접재료비"])}${check("eyunRound", "이윤 비율 사사오입")}</div>` +
`<button data-action="accom-option-ok">적 용</button> <button data-action="accom-option-cancel">취 소</button>`,
);
}
function set(name, value) {
const options = EstApp.state.project.options,
bucket = name in COMMON ? "Display" : "AccomOption";
const flag = typeof COMMON[name] === "boolean" || typeof ACCOM[name] === "boolean";
const typed = flag
? typeof value === "string"
? value === "1"
: Boolean(value)
: name === "color"
? value
: num(value);
options[bucket] = { ...(options[bucket] || {}), [name]: typed };
const box = document.getElementById("dialog-content");
if (name === "autoSave") box.querySelector('[data-ao="saveTime"]').disabled = !typed;
if (name === "gubun") {
box.querySelector('[data-ao="order"]').disabled = !typed;
box.querySelector('[data-ao="siljuk"]').disabled = !typed;
}
EstApp.render();
}
function close(apply) {
const options = EstApp.state.project.options;
if (opened) {
const now = { Display: options.Display, AccomOption: options.AccomOption };
for (const k of ["Display", "AccomOption"])
if (opened[k] === undefined) delete options[k];
else options[k] = opened[k];
// 적용: 연 때 값으로 돌린 뒤 바뀐 값을 한 번에 기록(되돌리기 한 단계)
if (apply) EstApp.change((project) => Object.assign(project.options, now), false);
}
opened = null;
EstApp.byId("dialog").close();
EstApp.render();
}
// Enter 뒤 셀 이동(ComboBoxEnter 62c+0x60): 표 안 입력칸에서 Enter 면 좌·우·위·아래 칸으로
function move(input, direction) {
const cell = input.closest("td"),
tr = cell?.parentElement;
if (!tr) return null;
const col = [...tr.children].indexOf(cell);
let target = null;
if (direction === 1 || direction === 2) {
const cells = [...tr.children];
for (
let c = col + (direction === 1 ? -1 : 1);
c >= 0 && c < cells.length;
c += direction === 1 ? -1 : 1
)
if ((target = cells[c].querySelector("input:not([disabled]),select:not([disabled])")))
break;
} else if (direction === 3 || direction === 4) {
for (
let r = direction === 3 ? tr.previousElementSibling : tr.nextElementSibling;
r;
r = direction === 3 ? r.previousElementSibling : r.nextElementSibling
)
if (
(target = r.children[col]?.querySelector("input:not([disabled]),select:not([disabled])"))
)
break;
}
return target;
}
function install() {
document.addEventListener("change", (event) => {
const t = event.target;
if (t.dataset?.ao === undefined) return;
set(t.dataset.ao, t.type === "checkbox" ? t.checked : t.value);
});
document.addEventListener("keydown", (event) => {
const state = EstApp.state;
if (state?.view !== "progress") return;
if (event.ctrlKey && event.key.toLowerCase() === "u") {
event.preventDefault();
dialog(state);
return;
}
const t = event.target;
if (event.key !== "Enter" || !t.matches?.("table.sheet input, table.sheet select")) return;
const direction = num(get(state.project.options).enter);
if (!direction) return;
const target = move(t, direction);
if (target) {
event.preventDefault();
t.dispatchEvent(new Event("change", { bubbles: true }));
setTimeout(() =>
(
document.querySelector(
`[data-key="${CSS.escape(target.dataset.key || "")}"][data-progress="${target.dataset.progress}"]`,
) || target
).focus(),
);
}
});
}
if (typeof document !== "undefined" && document.addEventListener) install();
function handle(name, state) {
if (name === "accom-option") dialog(state);
else if (name === "accom-option-ok") close(true);
else if (name === "accom-option-cancel") close(false);
else return false;
return true;
}
return { view, get, parts, move, handle, dialog };
})();
+1
View File
@@ -119,6 +119,7 @@ const EstProgressWonga = (() => {
b("pw-total", "공사비역산 자리수 맞추기"),
b("pw-init", "원가계산서 초기화"),
b("pw-undo", "되살리기"),
'<button data-action="accom-option" title="Ctrl+U">환경 설정</button>',
`<label><input type="checkbox" data-pw-sname ${lockFormula ? "checked" : ""}> 산출근거 약어로 표시</label>`,
round.wonga ? "<span>회차 서식 사용 중</span>" : "",
round.wongaTotalDigits ? `<span>역산 ${round.wongaTotalDigits}자리</span>` : "",
+1
View File
@@ -105,6 +105,7 @@ const allowed = new Set([
"native-excel-to-est.js",
"native-other-load.js",
"native-applied-cost.js",
"native-progress-view.js",
"native-actions-more.js",
"native-wonga-tools.js",
"native-estimate-print.js",