ListOrganizationsブロック
ListOrganizations は、BaseTable 上に構築された組織管理テーブルで、任意のタブ、検索チップ、行アクション、ページネーションを備えています。
インストール
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-list-organization-block
yarn add @nodeblocks/frontend-list-organization-block
pnpm add @nodeblocks/frontend-list-organization-block
bun add @nodeblocks/frontend-list-organization-block
必要なもの
| 項目 | 用途 |
|---|---|
labels | ヘッダーセル、ステータス、行アクション、検索フィールド、空状態のコピー |
data | テーブル行(ListOrganizationsRowData[]) |
listOrganizationsTitle | ヘッダータイトル |
searchValue + 検索チップ + ハンドラー (任意) | 制御された検索ドラフト、チップリスト、送信/削除フロー |
tabs + currentTab + onTabChange (任意) | 任意のワークフロー/ステータスフィルター UI |
statusMatch (任意) | 生の行 status 値をステータス列のアクティブ/非アクティブテキストにマッピングする |
pagination (任意) | 制御されたページ状態 |
| 行アクション + ナビゲーションハンドラー (任意) | labels.rowActions + resolveRowAction + アクションハンドラー、および行クリックナビゲーション |
ListOrganizations はテーブル状態を保持しません。タブ選択、検索ドラフト/チップ、ページネーションをアプリ側で管理し、ハンドラー(onTabChange、onSearchFieldChange、onSearchSubmit、pagination.onPageChange)を渡して状態を更新してください。
行は ListOrganizationsRowData の形状(id、createdAt、name、joinDate、status)を使用します。デフォルトでは、生成されるステータス列は生の値 'active' と 'inactive' をアクティブ/非アクティブラベルとして扱います。API が異なるステータス値を使用する場合は statusMatch を渡してください。statusMatch はタブのフィルターや行アクションの選択には使用されません。これらのチェックは生の row.status 値と整合させてください。
コード例
- クイックスタート
- ラベルと URL
- 複合コンポーネント
- ブロックオーバーライド
function Example() { const allOrganizationData = Array.from({length: 20}, (_, i) => ({ id: String(i + 1), createdAt: new Date(2024, 0, i + 1).toISOString(), name: `組織 ${i + 1}`, joinDate: new Date(2024, 0, i + 5).toISOString(), status: i % 2 === 0 ? 'active' : 'inactive', })); const tabs = [ {key: 'all', label: 'すべての組織'}, {key: 'active', label: 'アクティブ'}, {key: 'inactive', label: '非アクティブ'}, ]; const labels = { emptyStateMessage: '組織が見つかりません', searchFieldPlaceholder: '組織を検索...', rowActions: { activate: '組織を有効化', deactivate: '組織を無効化', }, headerRow: { createdAt: '作成日', name: '組織名', joinDate: '参加日', status: 'ステータス', }, cellData: { statusActive: 'アクティブ', statusInactive: '非アクティブ', }, }; const [currentTab, setCurrentTab] = React.useState('all'); const [searchValue, setSearchValue] = React.useState(''); const [currentPage, setCurrentPage] = React.useState(1); const [lastAction, setLastAction] = React.useState('検索・有効化・ナビゲーションのフィードバックがここに表示されます。'); const itemsPerPage = 5; const filteredByTab = allOrganizationData.filter(row => { if (currentTab === 'active') return row.status === 'active'; if (currentTab === 'inactive') return row.status === 'inactive'; return true; }); const filtered = searchValue.trim() ? filteredByTab.filter(row => { const keyword = searchValue.toLowerCase(); return row.name.toLowerCase().includes(keyword) || row.status.toLowerCase().includes(keyword); }) : filteredByTab; const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const start = (currentPage - 1) * itemsPerPage; const data = filtered.slice(start, start + itemsPerPage); return ( <> <ListOrganizations listOrganizationsTitle="組織管理" labels={labels} data={data} tabs={tabs} currentTab={currentTab} onTabChange={tab => { setCurrentTab(tab); setCurrentPage(1); }} searchValue={searchValue} onSearchFieldChange={setSearchValue} onSearchSubmit={() => setLastAction(`検索が送信されました: ${searchValue}`)} statusMatch={{active: 'active', inactive: 'inactive'}} resolveRowAction={row => (row.status === 'active' ? ['deactivate'] : ['activate'])} onItemActivate={rowId => setLastAction(`組織を有効化: ${rowId}`)} onItemDeactivate={rowId => setLastAction(`組織を無効化: ${rowId}`)} pagination={{ currentPage, totalPages, onPageChange: setCurrentPage, }} rowHref={row => `/organizations/${row.id}`} onNavigate={to => setLastAction(`ナビゲート: ${to}`)} /> <div style={{marginTop: 12, color: '#475569', fontSize: 13}}>{lastAction}</div> </> ); }
ラベル、ステータスマッチング、検索チップ、行ナビゲーションの動作をカスタマイズします。
function Example() { const labels = { emptyStateMessage: '一致する組織がありません', searchFieldPlaceholder: '名前またはステータスで検索', rowActions: { activate: '有効化', deactivate: '無効化', }, headerRow: { createdAt: '作成', name: '名前', joinDate: '参加', status: '現在のステータス', }, cellData: { statusActive: '有効', statusInactive: '無効', }, }; const data = [ { id: 'org-101', createdAt: new Date(2024, 3, 10).toISOString(), name: 'Basal Docs', joinDate: new Date(2024, 3, 12).toISOString(), status: 'enabled', }, ]; const chips = [{key: 'chip-1', label: 'Basal'}]; return ( <ListOrganizations listOrganizationsTitle="組織" labels={labels} data={data} searchValue="" onSearchFieldChange={() => {}} onSearchSubmit={() => {}} searchChipsTitle="検索キーワード" searchChips={chips} onSearchChipDelete={() => {}} statusMatch={{active: 'enabled', inactive: 'disabled'}} resolveRowAction={row => (row.status === 'enabled' ? ['deactivate'] : ['activate'])} onItemActivate={() => {}} onItemDeactivate={() => {}} rowHref={row => `/admin/organizations/${row.id}`} onNavigate={() => {}} /> ); }
searchChipsTitle、searchChips、onSearchChipDelete を渡すと、ヘッダーとタブの間に ListOrganizations.SearchChips がレンダリングされます。アクティブなキーワードがない場合はチップを省略してください。
ヘッダー/タブ/検索チップ/コンテンツ/ページネーションを明示的に構成します。
function Example() { const allOrganizationData = Array.from({length: 18}, (_, i) => ({ id: String(i + 1), createdAt: new Date(2024, 0, i + 1).toISOString(), name: `組織 ${i + 1}`, joinDate: new Date(2024, 0, i + 5).toISOString(), status: i % 2 === 0 ? 'active' : 'inactive', })); const labels = { emptyStateMessage: '組織が見つかりません', searchFieldPlaceholder: '組織を検索...', rowActions: { activate: '組織を有効化', deactivate: '組織を無効化', }, headerRow: { createdAt: '作成日', name: '組織名', joinDate: '参加日', status: 'ステータス', }, cellData: { statusActive: 'アクティブ', statusInactive: '非アクティブ', }, }; const tabs = [ {key: 'all', label: 'すべての組織'}, {key: 'active', label: 'アクティブ'}, {key: 'inactive', label: '非アクティブ'}, ]; const [currentTab, setCurrentTab] = React.useState('all'); const [currentPage, setCurrentPage] = React.useState(1); const [searchValue, setSearchValue] = React.useState(''); const [isLoading, setIsLoading] = React.useState(false); const [searchChips, setSearchChips] = React.useState([]); const [lastAction, setLastAction] = React.useState('検索を送信するか行アクションをトリガーすると、インラインステータスが更新されます。'); const itemsPerPage = 5; const filteredByTab = allOrganizationData.filter(row => { if (currentTab === 'active') return row.status === 'active'; if (currentTab === 'inactive') return row.status === 'inactive'; return true; }); const keywords = searchChips.map(chip => String(chip.label).toLowerCase()); const filtered = keywords.length ? filteredByTab.filter(row => keywords.some( keyword => row.name.toLowerCase().includes(keyword) || row.status.toLowerCase().includes(keyword), ), ) : filteredByTab; const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const pageStart = (currentPage - 1) * itemsPerPage; const data = filtered.slice(pageStart, pageStart + itemsPerPage); return ( <ListOrganizations listOrganizationsTitle="組織管理" labels={labels} data={data} tabs={tabs} currentTab={currentTab} onTabChange={tab => { setCurrentTab(tab); setCurrentPage(1); }} > <ListOrganizations.Header> <ListOrganizations.Title listOrganizationsTitle="組織管理" /> <ListOrganizations.Action labels={labels} searchValue={searchValue} onSearchFieldChange={setSearchValue} onSearchSubmit={() => { const trimmed = searchValue.trim(); if (trimmed && !searchChips.some(chip => chip.label === trimmed)) { setSearchChips(prev => [...prev, {key: `search-${Date.now()}`, label: trimmed}]); } setSearchValue(''); }} /> </ListOrganizations.Header> {searchChips.length > 0 ? ( <ListOrganizations.SearchChips searchChipsTitle="検索キーワード" searchChips={searchChips} onSearchChipDelete={chip => setSearchChips(prev => prev.filter(item => item.key !== chip.key))} /> ) : null} <ListOrganizations.Tabs /> <ListOrganizations.Content> {isLoading ? ( <ListOrganizations.Loader /> ) : ( <ListOrganizations.Table labels={labels} data={data} statusMatch={{active: 'active', inactive: 'inactive'}} resolveRowAction={row => (row.status === 'active' ? ['deactivate'] : ['activate'])} onItemActivate={rowId => setLastAction(`有効化: ${rowId}`)} onItemDeactivate={rowId => setLastAction(`無効化: ${rowId}`)} rowHref={row => `/organizations/${row.id}`} onNavigate={to => setLastAction(`ナビゲート: ${to}`)} /> )} </ListOrganizations.Content> <ListOrganizations.Pagination pagination={{ currentPage, totalPages, onPageChange: setCurrentPage, }} data={data} /> <div style={{marginTop: 12, color: '#475569', fontSize: 13}}>{lastAction}</div> </ListOrganizations> ); }
関数の子を使用してカスタムブロックを先頭に追加し、順序を制御します。
function Example() { const labels = { emptyStateMessage: '組織が見つかりません', searchFieldPlaceholder: '組織を検索...', rowActions: { activate: '組織を有効化', deactivate: '組織を無効化', }, headerRow: { createdAt: '作成日', name: '組織名', joinDate: '参加日', status: 'ステータス', }, cellData: { statusActive: 'アクティブ', statusInactive: '非アクティブ', }, }; const data = [ { id: '1', createdAt: new Date(2024, 0, 1).toISOString(), name: '組織 1', joinDate: new Date(2024, 0, 5).toISOString(), status: 'active', }, ]; return ( <ListOrganizations listOrganizationsTitle="組織管理" labels={labels} data={data} tabs={[{key: 'all', label: 'すべての組織'}]} currentTab="all" onTabChange={() => {}} searchValue="" onSearchFieldChange={() => {}} onSearchSubmit={() => {}} resolveRowAction={row => (row.status === 'active' ? ['deactivate'] : ['activate'])} onItemActivate={() => {}} onItemDeactivate={() => {}} > {({defaultBlocks, defaultBlockOrder}) => ({ blocks: { ...defaultBlocks, customNotification: ( <div style={{ marginBottom: 12, padding: 12, background: '#eef4ff', border: '1px solid #cddcff', borderRadius: 8, fontSize: 14, }} > カスタム通知: {data.length} 件の組織を表示中 </div> ), }, blockOrder: ['customNotification', ...defaultBlockOrder], })} </ListOrganizations> ); }
ブロックオーバーライドを使うタイミング
テーブルレイアウトの前にカスタムバナーやステータスパネルが必要であり、デフォルトのリスト動作を維持したい場合にオーバーライドを使用します。defaultBlockOrder は header、searchChips、tabs、content、pagination です。defaultBlocks には title、action、header、loader、table、searchChips、tabs、content、pagination も含まれます。
主要プロパティ
コアプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
labels | { emptyStateMessage: string; searchFieldPlaceholder: string; rowActions?: { activate: string; deactivate: string }; headerRow: { createdAt: string; name: string; joinDate: string; status: string }; cellData: { statusActive: string; statusInactive: string } } | はい | - | 検索、テーブルセル、行アクション、空状態の UI コピー |
data | ListOrganizationsRowData[] | はい | - | テーブル行 |
listOrganizationsTitle | ReactNode | はい | - | ヘッダータイトル |
isLoading | boolean | いいえ | undefined | コンテンツ/ページネーションのローディング状態 |
searchValue | string | いいえ | undefined | 制御された検索入力値 |
onSearchFieldChange | (value: string) => void | いいえ | undefined | 検索入力の変更ハンドラー |
onSearchSubmit | () => void | いいえ | undefined | 検索アイコンのクリックまたは Enter キーでトリガー |
searchChipsTitle | ReactNode | いいえ | undefined | アクティブな検索チップの上のラベル |
searchChips | BaseTableSearchChip[] | いいえ | undefined | アクティブな検索チップ |
onSearchChipDelete | (chip: BaseTableSearchChip, index: number, event: SyntheticEvent) => void | いいえ | undefined | チップ削除ハンドラー |
tabs | { key: string; label: string; isDisabled?: boolean; subtitle?: string }[] | いいえ | undefined | 任意のタブ定義 |
currentTab | string | いいえ | undefined | アクティブなタブキー |
onTabChange | (tab: string) => void | いいえ | undefined | タブ変更ハンドラー |
statusMatch | { active: string; inactive: string } | いいえ | { active: 'active', inactive: 'inactive' } | 生の row.status 値を生成されるステータス列のアクティブ/非アクティブテキストにマッピングする |
pagination | { currentPage: number; totalPages: number; onPageChange: (page: number) => void; className?: string } | いいえ | undefined | ページコントロール(ページは 1 から数える) |
rowHref | (row: ListOrganizationsRowData) => string | いいえ | undefined | 行リンクを構築する。onNavigate が必要 |
onNavigate | (to: string) => void | いいえ | undefined | 行クリックで rowHref が解決されたときに呼ばれる |
shouldShowDropdownMenu | (row: ListOrganizationsRowData) => boolean | いいえ | undefined | 行ごとに行メニューを表示/非表示 |
resolveRowAction | (row: ListOrganizationsRowData) => ('activate' | 'deactivate')[] | undefined | いいえ | undefined | レンダリングする行アクションの種類 |
onItemActivate | (rowId: string) => void | いいえ | undefined | activate アクションのハンドラー |
onItemDeactivate | (rowId: string) => void | いいえ | undefined | deactivate アクションのハンドラー |
ListOrganizationsRowData の形状:
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
id | string | はい | - | 一意の行 ID |
createdAt | string | はい | - | 作成日列で使用する ISO 日時文字列 |
name | string | はい | - | 組織名 |
joinDate | string | はい | - | 参加日列で使用する ISO 日時文字列 |
status | string | はい | - | 生のステータス値。statusMatch はステータス列の表示のみを制御し、タブと行アクションは独自の比較を使用する |
コンテンツプロパティ
labels キー:
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
labels.emptyStateMessage | string | はい | - | 空のテーブルのメッセージ |
labels.searchFieldPlaceholder | string | はい | - | 検索入力のプレースホルダー |
labels.rowActions.activate | string | いいえ | undefined | 有効化アクションのラベル |
labels.rowActions.deactivate | string | いいえ | undefined | 無効化アクションのラベル |
labels.headerRow.createdAt | string | はい | - | 作成日ヘッダー |
labels.headerRow.name | string | はい | - | 名前列ヘッダー |
labels.headerRow.joinDate | string | はい | - | 参加日ヘッダー |
labels.headerRow.status | string | はい | - | ステータス列ヘッダー |
labels.cellData.statusActive | string | はい | - | アクティブ行のセルラベル |
labels.cellData.statusInactive | string | はい | - | 非アクティブ行のセルラベル |
サブコンポーネント:
| コンポーネント | プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|---|
Title | listOrganizationsTitle | ReactNode | いいえ | ルートの listOrganizationsTitle(BaseTable の headerTitle 経由) | タイトルテキスト(children が優先) |
Title | children | ReactNode | いいえ | ルートのタイトル | カスタムタイトルマークアップ |
Action | labels | { emptyStateMessage: string; searchFieldPlaceholder: string; rowActions?: { activate: string; deactivate: string }; headerRow: { createdAt: string; name: string; joinDate: string; status: string }; cellData: { statusActive: string; statusInactive: string } } | いいえ | ルートの labels | 検索入力のコピー |
Action | searchValue | string | いいえ | ルートの searchValue | 制御された検索入力 |
Action | onSearchFieldChange | (value: string) => void | いいえ | ルートのハンドラー | 検索入力の変更 |
Action | onSearchSubmit | () => void | いいえ | ルートのハンドラー | 検索の送信 |
Action | children | ReactNode | いいえ | 組み込みの検索コントロール | カスタムアクションコンテンツ |
SearchChips | searchChipsTitle | ReactNode | いいえ | ルートの searchChipsTitle | チップグループのタイトル |
SearchChips | searchChips | BaseTableSearchChip[] | いいえ | ルートの searchChips | チップデータ |
SearchChips | onSearchChipDelete | (chip: BaseTableSearchChip, index: number, event: SyntheticEvent) => void | いいえ | ルートのハンドラー | チップ削除コールバック |
Tabs | value | string | いいえ | ルートの currentTab | アクティブなタブ値のオーバーライド |
Tabs | onChange | TabsProps['onChange'] | いいえ | ルートの onTabChange | MUI タブ変更コールバック |
Tabs | tabProps | MUI Tab props excluding label, value, and disabled | いいえ | undefined | 各 MUI タブに転送される props |
Table | labels | ルートの labels と同じ形状 | いいえ | ルートの labels | 列/行なしオーバーレイ/行アクションラベルを生成する |
Table | statusMatch | { active: string; inactive: string } | いいえ | { active: 'active', inactive: 'inactive' } | 生の row.status 値のステータス列表示マッピング |
Table | columns | BaseTableColumn<ListOrganizationsRowData>[] | いいえ | labels + statusMatch から生成 | グリッド列のオーバーライド |
Table | rowActions | (row: ListOrganizationsRowData) => BaseTableRowAction<ListOrganizationsRowData>[] | いいえ | 行アクションプロップから生成 | 行アクションのオーバーライド |
Table | actionColumn | BaseTableActionColumn | いいえ | 行アクションがある場合 { pin: 'right' } | アクション列の設定 |
Table | rowMenu | ReactNode or function | いいえ | デフォルトメニュー | カスタム行メニュー UI |
Table | onRowClick | (row: ListOrganizationsRowData) => void | いいえ | rowHref + onNavigate から導出 | 行クリックコールバック |
Loader | children | ReactNode | いいえ | デフォルトローダー | ローディングコンテンツ |
Pagination | pagination | { currentPage: number; totalPages: number; onPageChange: (page: number) => void; className?: string } | いいえ | ルートの pagination | ページネーションコントロール |
Pagination | data | ListOrganizationsRowData[] | いいえ | ルートの data | 行数のソース |
Pagination | isLoading | boolean | いいえ | ルートの isLoading | ローディング中または行がない場合はページネーションを非表示 |
Title、Action、Header、Loader、SearchChips、Tabs、Content、Table、Pagination は ListOrganizations.Title などです。ListOrganizations.Tabs はルートの tabs プロップからタブ定義を読み取ります。
レイアウトと構成プロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | いいえ | undefined | 複合 JSX の子、または blocks と blockOrder を返す関数オーバーライド |
className | string | いいえ | undefined | ルートコンテナ(nbb-list-organizations-container)のクラス |
sx | SxProps | いいえ | undefined | ルート用の MUI システムスタイル |
ListOrganizations は headerTitle、検索アクション、任意のタブ、行、行アクション、行なしオーバーレイ、任意のページネーションを備えた BaseTable ルートをレンダリングします。StackProps のパススルー(children を除く)を継承します。defaultBlockOrder は header、searchChips、tabs、content、pagination です。
デフォルト UI ブロック
| ブロック | ベース | 備考 |
|---|---|---|
ListOrganizations (ルート) | BaseTable | ルートテーブルラッパー |
ListOrganizations.Title | BaseTable.Header.Title | listOrganizationsTitle からのタイトル |
ListOrganizations.Action | BaseTable.Header.Actions + TextField | 検索アイコン送信付きの検索フィールド |
ListOrganizations.Header | BaseTable.Header | タイトル/アクションをラップ |
ListOrganizations.SearchChips | BaseTable.SearchChips | アクティブなキーワードチップブロック |
ListOrganizations.Tabs | BaseTable.Tabs | tabs からの任意のタブ |
ListOrganizations.Loader | BaseTable.Content.Loader | ローディング状態 |
ListOrganizations.Content | BaseTable.Content | コンテンツラッパー |
ListOrganizations.Table | BaseTable.Content.Grid | 生成された列/アクション付きデータグリッド |
ListOrganizations.Pagination | BaseTable.Pagination | ページコントロール |
| 行なしオーバーレイアイコン | PersonOutlined | data が空のときに使用 |
TypeScript
import {
ListOrganizations,
ListOrganizationsRowData,
BaseTableSearchChip,
} from '@nodeblocks/frontend-list-organization-block';
const rows: ListOrganizationsRowData[] = [
{
id: '1',
createdAt: new Date().toISOString(),
name: '組織 1',
joinDate: new Date().toISOString(),
status: 'active',
},
];
const chips: BaseTableSearchChip[] = [{key: 'keyword-1', label: '組織 1'}];
<ListOrganizations
listOrganizationsTitle="組織管理"
labels={{
emptyStateMessage: '組織が見つかりません',
searchFieldPlaceholder: '組織を検索...',
rowActions: {activate: '組織を有効化', deactivate: '組織を無効化'},
headerRow: {
createdAt: '作成日',
name: '組織名',
joinDate: '参加日',
status: 'ステータス',
},
cellData: {statusActive: 'アクティブ', statusInactive: '非アクティブ'},
}}
data={rows}
searchChipsTitle="検索キーワード"
searchChips={chips}
statusMatch={{active: 'active', inactive: 'inactive'}}
/>;