ボタンと処理を定義する
画面上部のアクション、組み込みの処理、独自処理の差し込み。
画面のボタンは actions に並べる。type がその挙動を決める。
actions:
- { id: create, type: create, label: 新規登録 }
- { id: export, type: export, label: CSV出力, config: { filename: 顧客一覧, bom: true } }
- { id: sync, type: plugin, plugin: syncCustomers, label: 外部連携 }6つの type で足りるか、足りないか
| type | 何をするか | 自分のコードが必要か |
|---|---|---|
create edit | フォームを開く | 不要 |
delete | 確認してから削除する | 不要 |
navigate | 別の画面へ移る | 不要 |
export | 画面の列と行から CSV を組む | 出力先の登録だけ |
plugin | 登録した処理を呼ぶ | 必要(処理の中身) |
つまり業務固有の処理は全部 plugin に落ちる。承認する、外部システムに送る、帳票を作る、といったものはここ。
plugin の中身は Framework の外に書く
plugin: syncCustomers は「syncCustomers という名前で登録された処理を呼ぶ」という指定でしかない。処理そのものは自分のコードで書いて、アプリ起動時に名前で登録する。Framework は業務ロジックを持たないので、ここが自分のコードとの境界になる。
定義側から渡せるのは config。同じ処理を画面ごとに少し変えたいときはここに差を書く。
CSV 出力は「画面の見えているもの」が出る
type: export は、その画面の列定義と検索結果から CSV を組む。だからロールで見えない列は CSV にも出ない。一覧の export は表示中のページではなく検索結果全体(limit まで)を出す。
config で filename / delimiter / newline / bom / raw(format を通さない生値)などを指定できる。Excel で開く前提なら bom: true。
ファイルを実際に書くのは利用者側。Framework はファイルシステムもダウンロードも知らないので、出力先を登録しておく必要がある。登録していなければ失敗する(onSuccess も動かない)。
押しても何も起きないボタンは、押す前に言う
type はどれも「その画面が持っているもの」に効く。持っていない画面に置くと、定義は通り、ボタンも出て、押すまで気づけない(押した人には壊れているように見える)。この枠組みで一番まずい転び方なので、npx hatake validate が押す前に言う。
| 書き方 | 押すとどうなるか | 規則 |
|---|---|---|
create を crud / master 以外に | 何も起きない(開く一覧が無い) | create-action-unusable |
export を表の無い画面に | 何も出ない(CSV にする行が無い) | export-without-rows |
print を report の無い画面に | 何も出ない(刷る紙が無い) | print-without-report |
plugin に plugin: を書き忘れ | 何も起きない(呼ぶ相手が無い) | plugin-without-name |
navigate の行き先がその画面自身 | 同じ画面がもう1枚開くだけ | navigate-to-self |
edit / delete を行の外に | 何も起きない(行の操作なので) | row-declaration-unused |
判定は1画面ぶんの情報だけで決まる(外の登録も、他の画面も見ない)。だから CI に置ける。
いま押せるかどうかを、定義で言う
「出荷済は却下できない」を、押してから断るのではなくボタンの活性で言う。条件の書き方は visibleWhen と同じ。
table:
rowActions: [openEntry]
actions:
- id: openEntry
type: navigate
label: 明細編集
page: order_entry
params: { id: "$row.orderNo" }
enabledWhen: { field: status, operator: notEquals, value: 出荷済 }判定する相手は置き場所で決まる。行アクションはその行、一括(scope: selection)は選んだ行全部、入力する画面(form / wizard)のボタンはいま入力されている値、読むだけの画面(detail)のボタンはいま開いているレコード。一覧の上のボタンには判定する相手が無いので、書いても効かない(validate が言う)。
| 何で出し分けるか | 使うもの |
|---|---|
| 誰が使えるか(見えるかどうか) | roles |
| いまの状態で押せるか(出たまま灰色) | enabledWhen |
| 1回で動かせる件数 | maxRows |
押せないボタンは消えない。灰色のまま出て、何の状態で決まるのかが添えられる(文言は書かなくてよい=条件から作る)。消してしまうと、その操作が在ること自体が分からなくなる。
一括は「選んだ行が全部満たすときだけ」押せる。合わない行が混ざっている間はボタンに件数が出る(「一括承認(3 件:1 件は条件に合いません)」)=選び直せば押せることが、押す前に読める。
入力する画面では、保存を挟まずに変わる
「下書きに直したら送信できる」は、直した時点で押せるようになる。ボタンが見ているのは、項目の出し分け(visibleWhen)や計算(computed)が見ているものと同じ record(いま入力されている値)だからで、保存を挟む必要はない。
enabledWhen: { field: status, operator: equals, value: 下書き }計算した項目でも書ける(enabledWhen: { field: 合計, operator: gt, value: 0 }=金額が入るまで送信させない)。新規入力のときだけ押せるボタンは { mode: create }。
保存しないと押せないままだと、押した人は「直したのに壊れている」と読む。画面に出ている値で判定するのがこの機能の意味です。読むだけの画面(detail)は入力が無いので、開いているレコードで判定する。
押せない理由に出る項目名は、その画面の見出しで言う(「いまは押せません(状態 によります)」)=定義に書いた label をそのまま使う。
params は navigate のためのもの
params は遷移先に渡す値。$row.<項目> / $record.<項目> で現在の行やレコードの値を埋められる。詳しくは「画面から画面へ遷移する」に書いた。
確認と成功後の動き
confirm と onSuccess はどの type にも書ける。ただし create / edit はフォームを開くだけで保存の成否がその時点では分からないので、onSuccess は動かない。詳しくは「確認ダイアログと成功後の動き」に書いた。
書けるキー
| キー | 書く場所 | 型 | 必須 | 既定値 | 有効なページ種別 | 説明 |
|---|---|---|---|---|---|---|
actions | crudPage | array → action | 任意 | — | crud | Page-level actions (buttons). |
actions | dashboardPage | array → action | 任意 | — | dashboard | — |
actions | detailPage | array → action | 任意 | — | detail | — |
actions | formPage | array → action | 任意 | — | form | — |
actions | masterPage | array → action | 任意 | — | master | — |
actions | reportPage | array → action | 任意 | — | report | — |
actions | searchPage | array → action | 任意 | — | search | Page-level actions; also referenced by table.rowActions. |
actions | wizardPage | array → action | 任意 | — | wizard | — |
params | action | object → action.params | 任意 | — | crud dashboard detail form master report search wizard | Route params for a navigate action; $row.id / $record.id template against the current row/record. |
params | actionSuccess | object → actionSuccess.params | 任意 | — | crud dashboard detail form master report search wizard | Route params for page; $row.id / $record.id template against the current row/record. |
plugin | action | string | 任意 | — | crud dashboard detail form master report search wizard | Registered action plugin key (used when type is 'plugin'). |
この表は spec/reference.json から生成している(JSON Schema が正)。手元では npx hatake reference <キー名> で同じものが引ける。
近い例
例は丸ごと写して直すのが一番速い。以下は CI で検証済み(そのまま動く形)。
| ファイル | 種別 | 画面 | どういうときに使うか |
|---|---|---|---|
customer_master.yaml | crud | 顧客マスタ | 検索して一覧に出して、その場で登録・修正・削除まで面倒を見る画面が欲しい |
dept_master.yaml | master | 部門マスタ | コードと名前だけの小さなマスタを、最小の定義でメンテしたい |
order_entry.yaml | form | 受注入力 | ヘッダと明細行を1画面で入力して、まとめて保存したい |
sales_report.yaml | report | 売上明細表 | 一覧の印刷版が欲しい。顧客ごとに小計を出して、CSV も落としたい、紙にも刷りたい |
sales_app.yaml | app | 販売管理 | 複数の画面をメニューで束ねて、1つのアプリにしたい。会社の色にもしたい |
よくある間違い
scope: selection に type: delete を書いて「一括削除」にする
なぜ駄目か 選んだ行にまとめて実行できるのは type: plugin だけなので、押しても実行されない。用意していないのは、取り消せない操作は事故が件数ぶん大きくなるから。1件ずつなら「押し間違えた」で済むが、全選択のあとの1回は戻せない。
こう直す 消すのは1件ずつ(table.rowActions の delete)。どうしてもまとめて消すなら type: plugin にして、消す条件と件数の上限をアプリ側で持つ(そこで確認と記録も残せる)。
page:
type: search
id: order_search
title: 受注照会
repository: orderRepository
key: orderNo
table:
rowActions: [delete]
columns:
- { field: orderNo, label: 受注番号 }
actions:
- { id: remove, type: delete, label: 削除, roles: [admin] }
- id: approveSelected
type: plugin
plugin: approveOrders
label: 一括承認
scope: selection
roles: [manager]
confirm: { message: 選んだ受注を承認します }一覧の画面に type: print の印刷ボタンを置く
なぜ駄目か 紙の形(用紙・1枚の行数・グループ・小計)を決めているのは report なので、report の無い画面には刷るものが無い。定義は通り、ボタンも出るが、押すと「このページでは刷れません」と言われる(押すまで分からない)。
こう直す 印刷は帳票(type: report)に置く。一覧をそのまま持ち出したいなら type: export(CSV)で、こちらはどの画面でも動く。
page:
type: report
id: sales_report
title: 売上明細表
repository: orderRepository
table:
columns:
- { field: amount, label: 金額, type: number }
report:
paper: { size: A4 }
rowsPerPage: 30
actions:
- { id: printPdf, type: print, label: 印刷 }spec/pitfalls.json から生成。各項目は CI で検証済み(間違いは本当に落ち、正しい方は本当に通る)。手元では npx hatake pitfalls <キー名>。
実物を見る
デモアプリの「受注入力」がこれを使っている。 デモを開く