@navette-js/actions 1.0.0-beta.1 → 1.0.0-beta.3
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 +2 -2
- package/lib/index.d.ts +42 -42
- package/lib/index.js +58 -59
- package/package.json +4 -5
- package/views/button.ejs +1 -1
- package/views/modal.ejs +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @navette-js/actions
|
|
2
2
|
|
|
3
|
-
Actions and modals for Navette
|
|
3
|
+
Actions and modals for Navette admin panels.
|
|
4
4
|
|
|
5
|
-
Part of [Navette](https://github.com/NavetteJS/navette),
|
|
5
|
+
Part of [Navette](https://github.com/NavetteJS/navette), admin panels for Node.js. Start a project with `npm create navette@latest`, or install the framework with `npm install @navette-js/panels`.
|
|
6
6
|
|
|
7
7
|
**Beta:** the API may still change between beta releases.
|
|
8
8
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ColorManager, type Lazy } from '@navette-js/support';
|
|
2
2
|
import type { ComponentContext, Renderable } from '@navette-js/schemas';
|
|
3
|
-
/**
|
|
3
|
+
/** Render hook slots of an action modal. Hooks receive the action as `data.action`. */
|
|
4
4
|
export declare const ActionsRenderHook: {
|
|
5
5
|
readonly MODAL_CUSTOM_CONTENT_AFTER: "actions::modal.custom-content.after";
|
|
6
6
|
readonly MODAL_CUSTOM_CONTENT_BEFORE: "actions::modal.custom-content.before";
|
|
@@ -10,7 +10,7 @@ export declare const ActionsRenderHook: {
|
|
|
10
10
|
readonly MODAL_SCHEMA_BEFORE: "actions::modal.schema.before";
|
|
11
11
|
};
|
|
12
12
|
export declare const useColorManager: (manager: ColorManager) => void;
|
|
13
|
-
/**
|
|
13
|
+
/** Execution context of a mounted action. */
|
|
14
14
|
export type ActionCallContext = {
|
|
15
15
|
livewire: any;
|
|
16
16
|
record: any;
|
|
@@ -18,7 +18,7 @@ export type ActionCallContext = {
|
|
|
18
18
|
data: Record<string, unknown>;
|
|
19
19
|
};
|
|
20
20
|
export declare class Action implements Renderable {
|
|
21
|
-
/**
|
|
21
|
+
/** Default setup for every instance created afterwards by `make()`, e.g. `Action.configureUsing((action) => action.size('sm'))`. */
|
|
22
22
|
static configureUsing<T>(this: abstract new (...args: any[]) => T, configure: (instance: T) => void): void;
|
|
23
23
|
private name;
|
|
24
24
|
private _label;
|
|
@@ -52,23 +52,23 @@ export declare class Action implements Renderable {
|
|
|
52
52
|
constructor(name: string);
|
|
53
53
|
static make(name: string): Action;
|
|
54
54
|
label(label: Lazy<string | null, ComponentContext>): this;
|
|
55
|
-
/**
|
|
55
|
+
/** Pass a function to make the color depend on the record (row actions). */
|
|
56
56
|
color(color: string | ((ctx: ComponentContext) => string | Promise<string>)): this;
|
|
57
57
|
size(size: string): this;
|
|
58
|
-
/**
|
|
58
|
+
/** Renders a `type="submit"` button that triggers the form's submit method, e.g. `.submit('authenticate')`. */
|
|
59
59
|
submit(form: string | null): this;
|
|
60
60
|
hidden(condition?: Lazy<boolean, ComponentContext>): this;
|
|
61
|
-
/**
|
|
61
|
+
/** Navigation action, rendered as `<a href wire:navigate>`. */
|
|
62
62
|
url(url: string | null | ((ctx: ComponentContext) => string | null)): this;
|
|
63
|
-
/**
|
|
63
|
+
/** A string: host component method called on click (`wire:click`). A function: the action handler, mounted through `mountAction()` (with its modal, if any). */
|
|
64
64
|
action(action: string | ((ctx: ActionCallContext) => unknown | Promise<unknown>) | null): this;
|
|
65
65
|
icon(name: string | null | ((ctx: ComponentContext) => string | null | Promise<string | null>)): this;
|
|
66
|
-
/**
|
|
66
|
+
/** Icon used when the action is a table row link. */
|
|
67
67
|
tableIcon(name: string | null): this;
|
|
68
68
|
private _iconButton;
|
|
69
|
-
/**
|
|
69
|
+
/** Icon-only row action; the label becomes the tooltip and accessible name. */
|
|
70
70
|
iconButton(condition?: boolean): this;
|
|
71
|
-
/**
|
|
71
|
+
/** Icon used when the action is a menu item (ActionGroup). */
|
|
72
72
|
groupedIcon(name: string | null): this;
|
|
73
73
|
requiresConfirmation(condition?: boolean): this;
|
|
74
74
|
modalHeading(heading: Lazy<string | null, ActionCallContext>): this;
|
|
@@ -77,56 +77,56 @@ export declare class Action implements Renderable {
|
|
|
77
77
|
modalIcon(name: string | null): this;
|
|
78
78
|
private _badge;
|
|
79
79
|
private _badgeColor;
|
|
80
|
-
/**
|
|
80
|
+
/** Badge (counter) shown on the button. */
|
|
81
81
|
badge(badge: string | number | null | (() => string | number | null | Promise<string | number | null>)): this;
|
|
82
82
|
badgeColor(color: string): this;
|
|
83
83
|
private _slideOver;
|
|
84
84
|
private _hasSubmit;
|
|
85
|
-
/**
|
|
85
|
+
/** Opens the modal as a slide-over panel. */
|
|
86
86
|
slideOver(condition?: boolean): this;
|
|
87
|
-
/** `false
|
|
87
|
+
/** `false`: read-only modal, without a submit button. */
|
|
88
88
|
modalSubmitAction(condition: boolean): this;
|
|
89
89
|
private _cancelLabel;
|
|
90
|
-
/**
|
|
90
|
+
/** Label of the modal's close button ("Cancel" by default). */
|
|
91
91
|
modalCancelActionLabel(label: string | null): this;
|
|
92
92
|
private _tooltip;
|
|
93
93
|
private _newTab;
|
|
94
|
-
/**
|
|
94
|
+
/** Tooltip shown on hover. */
|
|
95
95
|
tooltip(text: Lazy<string | null, ComponentContext>): this;
|
|
96
|
-
/**
|
|
96
|
+
/** Opens the action's URL in a new tab. */
|
|
97
97
|
openUrlInNewTab(condition?: boolean): this;
|
|
98
98
|
private tooltipAttribute;
|
|
99
99
|
private _modalIconColor;
|
|
100
|
-
/**
|
|
100
|
+
/** Color of the modal icon; defaults to the action's color. */
|
|
101
101
|
modalIconColor(color: string | null): this;
|
|
102
102
|
getColor(): string;
|
|
103
103
|
getLabel(ctx: ComponentContext): Promise<string>;
|
|
104
104
|
getIcon(ctx: ComponentContext): Promise<string | null>;
|
|
105
105
|
private _sort;
|
|
106
|
-
/**
|
|
106
|
+
/** Position in a menu (user menu, tenant menu); negative values go before the theme switcher or the tenant list. */
|
|
107
107
|
sort(sort: number): this;
|
|
108
108
|
getSort(): number;
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
110
|
+
* Item of a panel menu (user menu, tenant menu). Only URL actions are rendered: these menus are not components
|
|
111
|
+
* that can mount actions.
|
|
112
112
|
*/
|
|
113
113
|
toMenuItemHtml(ctx: ComponentContext, options?: {
|
|
114
114
|
navigate?: boolean;
|
|
115
115
|
}): Promise<string>;
|
|
116
|
-
/**
|
|
116
|
+
/** A computed URL depends on the record, so the host checks it again on mount (`resolveUrl`). */
|
|
117
117
|
isMountable(): boolean;
|
|
118
|
-
/**
|
|
118
|
+
/** URL of the action in this context; an action with a URL is a link, never a mounted action. */
|
|
119
119
|
resolveUrl(ctx: ComponentContext): string | null;
|
|
120
120
|
hasModal(): boolean;
|
|
121
|
-
/**
|
|
121
|
+
/** Form shown in the modal. With a function, the components are rebuilt on every request. */
|
|
122
122
|
schema(components: any[] | ((schema: any) => any), columns?: Record<string, number> | number | null): this;
|
|
123
|
-
/**
|
|
123
|
+
/** Applies the action's form to the host's schema; the function returns either components or the configured schema. */
|
|
124
124
|
applySchema(schema: any): any | null;
|
|
125
|
-
/**
|
|
125
|
+
/** Extra buttons in the modal footer, after the submit button. */
|
|
126
126
|
extraModalFooterActions(actions: Action[]): this;
|
|
127
|
-
/**
|
|
127
|
+
/** Initial form state (e.g. from the record). */
|
|
128
128
|
fillForm(resolver: (ctx: ActionCallContext) => Record<string, unknown> | Promise<Record<string, unknown>>): this;
|
|
129
|
-
/**
|
|
129
|
+
/** Modal width (`xs` … `7xl`). Defaults to `md` for a confirmation and `4xl` for a form. */
|
|
130
130
|
modalWidth(width: string | null): this;
|
|
131
131
|
hasSchema(): boolean;
|
|
132
132
|
getFormFill(ctx: ActionCallContext): Promise<Record<string, unknown> | null>;
|
|
@@ -134,50 +134,50 @@ export declare class Action implements Renderable {
|
|
|
134
134
|
private _mutateData;
|
|
135
135
|
private _before;
|
|
136
136
|
private _after;
|
|
137
|
-
/**
|
|
137
|
+
/** Transforms the validated form data before the handler runs (add the author, normalize a value…). */
|
|
138
138
|
mutateDataUsing(fn: (data: Record<string, unknown>, ctx: ActionCallContext) => Record<string, unknown> | Promise<Record<string, unknown>>): this;
|
|
139
|
-
/**
|
|
139
|
+
/** Runs right before the handler; throwing cancels the action. */
|
|
140
140
|
before(fn: (ctx: ActionCallContext) => unknown | Promise<unknown>): this;
|
|
141
|
-
/**
|
|
141
|
+
/** Runs after the handler succeeds (logging, extra notification…). */
|
|
142
142
|
after(fn: (ctx: ActionCallContext) => unknown | Promise<unknown>): this;
|
|
143
143
|
call(ctx: ActionCallContext): Promise<unknown>;
|
|
144
|
-
/**
|
|
144
|
+
/** Menu item of a table bulk action. The click goes through the table's Alpine `mountAction()`, which sends the selection. */
|
|
145
145
|
toGroupedHtml(ctx: ComponentContext, context: Record<string, unknown>): Promise<string>;
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
147
|
+
* Menu item of a row action inside an ActionGroup. Disabled-capable and key-bound items use a different class order
|
|
148
|
+
* than plain items; front-end CSS and tests rely on the exact markup.
|
|
149
149
|
*/
|
|
150
150
|
toRecordGroupedHtml(ctx: ComponentContext, context: Record<string, unknown>, wireKey: string): Promise<string>;
|
|
151
|
-
/**
|
|
151
|
+
/** Small link (or icon button) for a table row action. */
|
|
152
152
|
toLinkHtml(ctx: ComponentContext, context: Record<string, unknown>, wireKey: string): Promise<string>;
|
|
153
|
-
/**
|
|
153
|
+
/** Modal of a mounted action; `content` is the rendered form, if any. */
|
|
154
154
|
toModalHtml(ctx: ActionCallContext, index: number, content?: string | null): Promise<string>;
|
|
155
155
|
alpineClickHandler(handler: string | null): this;
|
|
156
156
|
private _livewireTarget;
|
|
157
157
|
private _iconAfter;
|
|
158
|
-
/**
|
|
158
|
+
/** Loading indicator target (`wire:target`) when the click is handled in JavaScript. */
|
|
159
159
|
livewireTarget(target: string | null): this;
|
|
160
|
-
/**
|
|
160
|
+
/** `'after'` puts the icon (and loading indicator) after the label. */
|
|
161
161
|
iconPosition(position: 'before' | 'after'): this;
|
|
162
162
|
keyBindings(bindings: string[]): this;
|
|
163
163
|
getName(): string;
|
|
164
164
|
private _authorize;
|
|
165
|
-
/**
|
|
165
|
+
/** Authorization check, separate from `visible` / `hidden`: when it fails, the action is hidden and the server refuses it. */
|
|
166
166
|
authorize(condition: Lazy<boolean, ComponentContext>): this;
|
|
167
167
|
isVisible(ctx: ComponentContext): Promise<boolean>;
|
|
168
168
|
visible(condition?: Lazy<boolean, ComponentContext>): this;
|
|
169
|
-
/**
|
|
169
|
+
/** Shown but unusable; the server also refuses to mount it. */
|
|
170
170
|
disabled(condition?: Lazy<boolean, ComponentContext>): this;
|
|
171
171
|
isDisabled(ctx: ComponentContext): Promise<boolean>;
|
|
172
|
-
/**
|
|
172
|
+
/** Renders the action as a button (or as a link when it has a URL). */
|
|
173
173
|
toHtml(ctx: ComponentContext, explicitMountContext?: Record<string, unknown> | null): Promise<string>;
|
|
174
174
|
}
|
|
175
|
-
/**
|
|
175
|
+
/** Group of actions collapsed into a dropdown behind a "⋮" icon button. */
|
|
176
176
|
export declare class ActionGroup {
|
|
177
177
|
private actions;
|
|
178
178
|
constructor(actions: Action[]);
|
|
179
179
|
static make(actions: Action[]): ActionGroup;
|
|
180
180
|
getActions(): Action[];
|
|
181
|
-
/** `keyOf`
|
|
181
|
+
/** `keyOf` returns each action's `wire:key` (it depends on the record). */
|
|
182
182
|
toHtml(ctx: ComponentContext, context: Record<string, unknown>, keyOf: (action: Action) => string): Promise<string>;
|
|
183
183
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// @navette-js/actions —
|
|
1
|
+
// @navette-js/actions — actions: buttons, links, menu items and modal actions.
|
|
2
2
|
import { Attributes, e, evaluate, icon, js, loadingIndicator, renderHook, views, configured, configureUsing } from '@navette-js/support';
|
|
3
3
|
import { t } from '@navette-js/support/i18n';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
views.namespace('filament-actions', fileURLToPath(new URL('../views', import.meta.url)));
|
|
6
|
-
/**
|
|
6
|
+
/** Render hook slots of an action modal. Hooks receive the action as `data.action`. */
|
|
7
7
|
export const ActionsRenderHook = {
|
|
8
8
|
MODAL_CUSTOM_CONTENT_AFTER: 'actions::modal.custom-content.after',
|
|
9
9
|
MODAL_CUSTOM_CONTENT_BEFORE: 'actions::modal.custom-content.before',
|
|
@@ -15,7 +15,7 @@ export const ActionsRenderHook = {
|
|
|
15
15
|
let colorManager = null;
|
|
16
16
|
export const useColorManager = (manager) => { colorManager = manager; };
|
|
17
17
|
export class Action {
|
|
18
|
-
/**
|
|
18
|
+
/** Default setup for every instance created afterwards by `make()`, e.g. `Action.configureUsing((action) => action.size('sm'))`. */
|
|
19
19
|
static configureUsing(configure) { configureUsing(this, configure); }
|
|
20
20
|
name;
|
|
21
21
|
_label = null;
|
|
@@ -49,7 +49,7 @@ export class Action {
|
|
|
49
49
|
constructor(name) { this.name = name; }
|
|
50
50
|
static make(name) { return configured(new Action(name)); }
|
|
51
51
|
label(label) { this._label = label; return this; }
|
|
52
|
-
/**
|
|
52
|
+
/** Pass a function to make the color depend on the record (row actions). */
|
|
53
53
|
color(color) { if (typeof color === 'function')
|
|
54
54
|
this._lazyColor = color;
|
|
55
55
|
else {
|
|
@@ -57,17 +57,17 @@ export class Action {
|
|
|
57
57
|
this._lazyColor = null;
|
|
58
58
|
} return this; }
|
|
59
59
|
size(size) { this._size = size; return this; }
|
|
60
|
-
/**
|
|
60
|
+
/** Renders a `type="submit"` button that triggers the form's submit method, e.g. `.submit('authenticate')`. */
|
|
61
61
|
submit(form) { this._submit = form; return this; }
|
|
62
62
|
hidden(condition = true) { this._hidden = condition; return this; }
|
|
63
|
-
/**
|
|
63
|
+
/** Navigation action, rendered as `<a href wire:navigate>`. */
|
|
64
64
|
url(url) { if (typeof url === 'function')
|
|
65
65
|
this._lazyUrl = url;
|
|
66
66
|
else {
|
|
67
67
|
this._url = url;
|
|
68
68
|
this._lazyUrl = null;
|
|
69
69
|
} return this; }
|
|
70
|
-
/**
|
|
70
|
+
/** A string: host component method called on click (`wire:click`). A function: the action handler, mounted through `mountAction()` (with its modal, if any). */
|
|
71
71
|
action(action) {
|
|
72
72
|
if (typeof action === 'function') {
|
|
73
73
|
this._handler = action;
|
|
@@ -85,12 +85,12 @@ export class Action {
|
|
|
85
85
|
this._icon = name;
|
|
86
86
|
this._lazyIcon = null;
|
|
87
87
|
} return this; }
|
|
88
|
-
/**
|
|
88
|
+
/** Icon used when the action is a table row link. */
|
|
89
89
|
tableIcon(name) { this._tableIcon = name; return this; }
|
|
90
90
|
_iconButton = false;
|
|
91
|
-
/**
|
|
91
|
+
/** Icon-only row action; the label becomes the tooltip and accessible name. */
|
|
92
92
|
iconButton(condition = true) { this._iconButton = condition; return this; }
|
|
93
|
-
/**
|
|
93
|
+
/** Icon used when the action is a menu item (ActionGroup). */
|
|
94
94
|
groupedIcon(name) { this._groupedIcon = name; return this; }
|
|
95
95
|
requiresConfirmation(condition = true) { this._requiresConfirmation = condition; return this; }
|
|
96
96
|
modalHeading(heading) { this._modalHeading = heading; return this; }
|
|
@@ -99,23 +99,23 @@ export class Action {
|
|
|
99
99
|
modalIcon(name) { this._modalIcon = name; return this; }
|
|
100
100
|
_badge = null;
|
|
101
101
|
_badgeColor = 'primary';
|
|
102
|
-
/**
|
|
102
|
+
/** Badge (counter) shown on the button. */
|
|
103
103
|
badge(badge) { this._badge = badge; return this; }
|
|
104
104
|
badgeColor(color) { this._badgeColor = color; return this; }
|
|
105
105
|
_slideOver = false;
|
|
106
106
|
_hasSubmit = true;
|
|
107
|
-
/**
|
|
107
|
+
/** Opens the modal as a slide-over panel. */
|
|
108
108
|
slideOver(condition = true) { this._slideOver = condition; return this; }
|
|
109
|
-
/** `false
|
|
109
|
+
/** `false`: read-only modal, without a submit button. */
|
|
110
110
|
modalSubmitAction(condition) { this._hasSubmit = condition; return this; }
|
|
111
111
|
_cancelLabel = null;
|
|
112
|
-
/**
|
|
112
|
+
/** Label of the modal's close button ("Cancel" by default). */
|
|
113
113
|
modalCancelActionLabel(label) { this._cancelLabel = label; return this; }
|
|
114
114
|
_tooltip = null;
|
|
115
115
|
_newTab = false;
|
|
116
|
-
/**
|
|
116
|
+
/** Tooltip shown on hover. */
|
|
117
117
|
tooltip(text) { this._tooltip = text; return this; }
|
|
118
|
-
/**
|
|
118
|
+
/** Opens the action's URL in a new tab. */
|
|
119
119
|
openUrlInNewTab(condition = true) { this._newTab = condition; return this; }
|
|
120
120
|
async tooltipAttribute(ctx) {
|
|
121
121
|
const text = await evaluate(this._tooltip, ctx);
|
|
@@ -126,22 +126,22 @@ export class Action {
|
|
|
126
126
|
}`)}" ` : '';
|
|
127
127
|
}
|
|
128
128
|
_modalIconColor = null;
|
|
129
|
-
/**
|
|
129
|
+
/** Color of the modal icon; defaults to the action's color. */
|
|
130
130
|
modalIconColor(color) { this._modalIconColor = color; return this; }
|
|
131
131
|
getColor() { return this._color; }
|
|
132
132
|
async getLabel(ctx) { return (await evaluate(this._label, ctx)) ?? this.getDefaultLabel(); }
|
|
133
133
|
async getIcon(ctx) { return this._lazyIcon ? this._lazyIcon(ctx) : this._icon; }
|
|
134
134
|
_sort = 0;
|
|
135
|
-
/**
|
|
135
|
+
/** Position in a menu (user menu, tenant menu); negative values go before the theme switcher or the tenant list. */
|
|
136
136
|
sort(sort) { this._sort = sort; return this; }
|
|
137
137
|
getSort() { return this._sort; }
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
139
|
+
* Item of a panel menu (user menu, tenant menu). Only URL actions are rendered: these menus are not components
|
|
140
|
+
* that can mount actions.
|
|
141
141
|
*/
|
|
142
142
|
async toMenuItemHtml(ctx, options = {}) {
|
|
143
143
|
if (!colorManager)
|
|
144
|
-
throw new Error('
|
|
144
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
145
145
|
const href = this.resolveUrl(ctx);
|
|
146
146
|
if (!href || !/^(\/(?!\/)|https?:\/\/)/.test(href))
|
|
147
147
|
return '';
|
|
@@ -150,25 +150,25 @@ export class Action {
|
|
|
150
150
|
const glyph = iconName ? icon(iconName, { attributes: new Attributes().class(colors.length ? ['fi-color', `fi-color-${this._color}`] : []) }) : '';
|
|
151
151
|
return `<a href="${e(href)}"${this._newTab ? ' target="_blank"' : options.navigate ? ' wire:navigate' : ''} class="${['fi-dropdown-list-item', 'fi-ac-grouped-action', ...colors].join(' ')} " role="menuitem" tabindex="-1">${glyph}<span class="fi-dropdown-list-item-label">${e(await this.getLabel(ctx))}</span></a>`;
|
|
152
152
|
}
|
|
153
|
-
/**
|
|
153
|
+
/** A computed URL depends on the record, so the host checks it again on mount (`resolveUrl`). */
|
|
154
154
|
isMountable() { return (this._handler !== null || this._schema !== null) && this._url === null; }
|
|
155
|
-
/**
|
|
155
|
+
/** URL of the action in this context; an action with a URL is a link, never a mounted action. */
|
|
156
156
|
resolveUrl(ctx) { return this._lazyUrl ? this._lazyUrl(ctx) : this._url; }
|
|
157
157
|
hasModal() { return this._requiresConfirmation || this._schema !== null; }
|
|
158
|
-
/**
|
|
158
|
+
/** Form shown in the modal. With a function, the components are rebuilt on every request. */
|
|
159
159
|
schema(components, columns = {}) { this._schema = typeof components === 'function' ? components : () => components; this._schemaColumns = columns; return this; }
|
|
160
|
-
/**
|
|
160
|
+
/** Applies the action's form to the host's schema; the function returns either components or the configured schema. */
|
|
161
161
|
applySchema(schema) {
|
|
162
162
|
if (!this._schema)
|
|
163
163
|
return null;
|
|
164
164
|
const result = this._schema(schema);
|
|
165
165
|
return Array.isArray(result) ? schema.columns(this._schemaColumns).components(result) : result;
|
|
166
166
|
}
|
|
167
|
-
/**
|
|
167
|
+
/** Extra buttons in the modal footer, after the submit button. */
|
|
168
168
|
extraModalFooterActions(actions) { this._extraModalFooterActions = actions; return this; }
|
|
169
|
-
/**
|
|
169
|
+
/** Initial form state (e.g. from the record). */
|
|
170
170
|
fillForm(resolver) { this._fillForm = resolver; return this; }
|
|
171
|
-
/**
|
|
171
|
+
/** Modal width (`xs` … `7xl`). Defaults to `md` for a confirmation and `4xl` for a form. */
|
|
172
172
|
modalWidth(width) { this._modalWidth = width; return this; }
|
|
173
173
|
hasSchema() { return this._schema !== null; }
|
|
174
174
|
async getFormFill(ctx) { return this._fillForm ? this._fillForm(ctx) : null; }
|
|
@@ -176,11 +176,11 @@ export class Action {
|
|
|
176
176
|
_mutateData = null;
|
|
177
177
|
_before = null;
|
|
178
178
|
_after = null;
|
|
179
|
-
/**
|
|
179
|
+
/** Transforms the validated form data before the handler runs (add the author, normalize a value…). */
|
|
180
180
|
mutateDataUsing(fn) { this._mutateData = fn; return this; }
|
|
181
|
-
/**
|
|
181
|
+
/** Runs right before the handler; throwing cancels the action. */
|
|
182
182
|
before(fn) { this._before = fn; return this; }
|
|
183
|
-
/**
|
|
183
|
+
/** Runs after the handler succeeds (logging, extra notification…). */
|
|
184
184
|
after(fn) { this._after = fn; return this; }
|
|
185
185
|
async call(ctx) {
|
|
186
186
|
if (!this._handler)
|
|
@@ -192,10 +192,10 @@ export class Action {
|
|
|
192
192
|
await this._after?.(ctx);
|
|
193
193
|
return result;
|
|
194
194
|
}
|
|
195
|
-
/**
|
|
195
|
+
/** Menu item of a table bulk action. The click goes through the table's Alpine `mountAction()`, which sends the selection. */
|
|
196
196
|
async toGroupedHtml(ctx, context) {
|
|
197
197
|
if (!colorManager)
|
|
198
|
-
throw new Error('
|
|
198
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
199
199
|
const expression = `mountAction(${js(this.name)}, {}, ${js(context)})`;
|
|
200
200
|
const attributes = new Attributes()
|
|
201
201
|
.merge({ role: 'menuitem', tabindex: '-1', type: 'button', 'wire:loading.attr': 'aria-disabled', 'wire:loading.class': 'fi-disabled', 'wire:target': expression })
|
|
@@ -205,12 +205,12 @@ export class Action {
|
|
|
205
205
|
return `<button ${attributes.toHtml()}>${iconHtml}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': expression }))}<span class="fi-dropdown-list-item-label">${e((await evaluate(this._label, ctx)) ?? this.getDefaultLabel())}</span></button>`;
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
208
|
+
* Menu item of a row action inside an ActionGroup. Disabled-capable and key-bound items use a different class order
|
|
209
|
+
* than plain items; front-end CSS and tests rely on the exact markup.
|
|
210
210
|
*/
|
|
211
211
|
async toRecordGroupedHtml(ctx, context, wireKey) {
|
|
212
212
|
if (!colorManager)
|
|
213
|
-
throw new Error('
|
|
213
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
214
214
|
const color = this._lazyColor ? await this._lazyColor(ctx) : this._color;
|
|
215
215
|
const iconName = this._lazyIcon ? await this._lazyIcon(ctx) : (this._groupedIcon ?? this._icon);
|
|
216
216
|
const expression = e(`mountAction(${js(this.name)}, {}, ${js(context)})`);
|
|
@@ -218,17 +218,17 @@ export class Action {
|
|
|
218
218
|
const label = (await evaluate(this._label, ctx)) ?? this.getDefaultLabel();
|
|
219
219
|
const iconHtml = iconName ? icon(iconName, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': `mountAction(${js(this.name)}, {}, ${js(context)})` }).class(color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []) }) : '';
|
|
220
220
|
const inner = `${iconHtml}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': `mountAction(${js(this.name)}, {}, ${js(context)})` }))}<span class="fi-dropdown-list-item-label">${e(label)}</span>`;
|
|
221
|
-
//
|
|
221
|
+
// Navigation action: a plain link, nothing is mounted on the server.
|
|
222
222
|
const href = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
|
|
223
223
|
if (href) {
|
|
224
224
|
const plainIcon = iconName ? icon(iconName, { attributes: new Attributes().class(color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []) }) : '';
|
|
225
|
-
//
|
|
225
|
+
// New-tab link: no `wire:navigate`.
|
|
226
226
|
if (this._newTab)
|
|
227
227
|
return `<a href="${e(href)}" target="_blank" role="menuitem" tabindex="-1" wire:key="${e(wireKey)}" class="${['fi-ac-grouped-action', 'fi-dropdown-list-item', ...colors].join(' ')}">${plainIcon}<span class="fi-dropdown-list-item-label">${e(label)}</span></a>`;
|
|
228
228
|
return `<a href="${e(href)}" wire:navigate wire:key="${e(wireKey)}" class="${['fi-dropdown-list-item', 'fi-ac-grouped-action', ...colors].join(' ')}" role="menuitem" tabindex="-1">${plainIcon}<span class="fi-dropdown-list-item-label">${e(label)}</span></a>`;
|
|
229
229
|
}
|
|
230
230
|
if (this._disabled !== false) {
|
|
231
|
-
//
|
|
231
|
+
// Conditionally disabled: this markup is used whether or not the action ends up disabled.
|
|
232
232
|
const disabled = await this.isDisabled(ctx);
|
|
233
233
|
return `<button ${disabled ? 'aria-disabled="true" ' : ''}role="menuitem" tabindex="-1" type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${['fi-ac-grouped-action', 'fi-dropdown-list-item', disabled ? 'fi-disabled' : null, ...colors].filter(Boolean).join(' ')}" wire:click="${expression}" wire:key="${e(wireKey)}">${inner}</button>`;
|
|
234
234
|
}
|
|
@@ -237,10 +237,10 @@ export class Action {
|
|
|
237
237
|
}
|
|
238
238
|
return `<button type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" wire:click="${expression}" wire:key="${e(wireKey)}" class="${['fi-dropdown-list-item', 'fi-ac-grouped-action', ...colors].join(' ')}" role="menuitem" tabindex="-1">${inner}</button>`;
|
|
239
239
|
}
|
|
240
|
-
/**
|
|
240
|
+
/** Small link (or icon button) for a table row action. */
|
|
241
241
|
async toLinkHtml(ctx, context, wireKey) {
|
|
242
242
|
if (!colorManager)
|
|
243
|
-
throw new Error('
|
|
243
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
244
244
|
const color = this._lazyColor ? await this._lazyColor(ctx) : this._color;
|
|
245
245
|
const iconName = this._lazyIcon ? await this._lazyIcon(ctx) : (this._tableIcon ?? this._icon);
|
|
246
246
|
const raw = `mountAction(${js(this.name)}, {}, ${js(context)})`;
|
|
@@ -253,27 +253,26 @@ export class Action {
|
|
|
253
253
|
return `<button title="${e(label)}" aria-label="${e(label)}" ${disabled ? 'disabled ' : ''}type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${buttonClasses}"${disabled ? '' : ` wire:click="${e(raw)}"`} wire:key="${e(wireKey)}">${glyph}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': raw }))}</button>`;
|
|
254
254
|
}
|
|
255
255
|
const iconHtml = iconName ? icon(iconName, { size: 'sm', attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': raw }) }) : '';
|
|
256
|
-
// Action de navigation : un lien vers la page, sans montage côté serveur.
|
|
257
256
|
const href = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
|
|
258
257
|
const tooltip = await this.tooltipAttribute(ctx);
|
|
259
|
-
//
|
|
258
|
+
// Only local or http(s) URLs become links (no `javascript:` or protocol-relative URLs).
|
|
260
259
|
if (href && /^(\/(?!\/)|https?:\/\/)/.test(href))
|
|
261
260
|
return `<a href="${e(href)}" ${this._newTab ? 'target="_blank" ' : ''}${tooltip}${this._newTab ? '' : 'wire:navigate '}wire:key="${e(wireKey)}" class="${classes}">${iconName ? icon(iconName, { size: 'sm' }) : ''}<span class="fi-link-label">${e(label)}</span></a>`;
|
|
262
261
|
const keys = this._keyBindings.length ? `x-bind:id="$id('key-bindings')" x-mousetrap.global.${this._keyBindings.map((k) => k.replace(/\+/g, '-')).join('.')}="document.getElementById($el.id)?.click()" ` : '';
|
|
263
262
|
return `<button ${keys}${tooltip}type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${classes}" wire:click="${e(raw)}" wire:key="${e(wireKey)}">${iconHtml}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': raw }), 'sm')}<span class="fi-link-label">${e(label)}</span></button>`;
|
|
264
263
|
}
|
|
265
|
-
/**
|
|
264
|
+
/** Modal of a mounted action; `content` is the rendered form, if any. */
|
|
266
265
|
async toModalHtml(ctx, index, content = null) {
|
|
267
266
|
if (!colorManager)
|
|
268
|
-
throw new Error('
|
|
267
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
269
268
|
const schemaCtx = { livewire: ctx.livewire, component: null, operation: null, record: ctx.record, get: () => undefined, state: undefined };
|
|
270
269
|
const label = (await evaluate(this._label, schemaCtx)) ?? this.getDefaultLabel();
|
|
271
270
|
const color = this._lazyColor ? await this._lazyColor(schemaCtx) : this._color;
|
|
272
271
|
const confirmation = this._requiresConfirmation;
|
|
273
|
-
//
|
|
272
|
+
// A confirmation is centered (md); a form is start-aligned (4xl), except at small widths.
|
|
274
273
|
const width = this._modalWidth ?? (confirmation ? 'md' : '4xl');
|
|
275
274
|
const submitLabel = this._modalSubmitActionLabel ?? t(confirmation ? 'filament-actions::modal.actions.confirm.label' : 'filament-actions::modal.actions.submit.label');
|
|
276
|
-
//
|
|
275
|
+
// Hook order: custom content, schema, content footer, each between its before/after slots.
|
|
277
276
|
const scopes = ctx.livewire.getRenderHookScopes?.() ?? [];
|
|
278
277
|
const hook = (name) => renderHook(name, scopes, { action: this });
|
|
279
278
|
const hooked = hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_BEFORE) + hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_AFTER)
|
|
@@ -288,31 +287,31 @@ export class Action {
|
|
|
288
287
|
description: (await evaluate(this._modalDescription, ctx)) ?? (this._requiresConfirmation ? t('filament-actions::modal.confirmation') : null),
|
|
289
288
|
iconName: this._modalIcon ?? (this._requiresConfirmation ? 'o-exclamation-triangle' : null), color,
|
|
290
289
|
cancel: await Action.make('cancel').label(this._cancelLabel ?? t('filament-actions::modal.actions.cancel.label')).color('gray').alpineClickHandler('close()').toHtml(schemaCtx),
|
|
291
|
-
//
|
|
290
|
+
// The submit button of a gray action uses the primary color.
|
|
292
291
|
submit: !this._hasSubmit ? '' : (await Action.make('submit').label(submitLabel).color(color === 'gray' ? 'primary' : color).submit('callMountedAction').toHtml(schemaCtx)) + (await Promise.all(this._extraModalFooterActions.map((a) => a.toHtml(schemaCtx)))).join(''),
|
|
293
292
|
});
|
|
294
293
|
}
|
|
295
294
|
alpineClickHandler(handler) { this._alpineClick = handler; return this; }
|
|
296
295
|
_livewireTarget = null;
|
|
297
296
|
_iconAfter = false;
|
|
298
|
-
/**
|
|
297
|
+
/** Loading indicator target (`wire:target`) when the click is handled in JavaScript. */
|
|
299
298
|
livewireTarget(target) { this._livewireTarget = target; return this; }
|
|
300
|
-
/**
|
|
299
|
+
/** `'after'` puts the icon (and loading indicator) after the label. */
|
|
301
300
|
iconPosition(position) { this._iconAfter = position === 'after'; return this; }
|
|
302
301
|
keyBindings(bindings) { this._keyBindings = bindings; return this; }
|
|
303
302
|
getName() { return this.name; }
|
|
304
303
|
_authorize = true;
|
|
305
|
-
/**
|
|
304
|
+
/** Authorization check, separate from `visible` / `hidden`: when it fails, the action is hidden and the server refuses it. */
|
|
306
305
|
authorize(condition) { this._authorize = condition; return this; }
|
|
307
306
|
async isVisible(ctx) { return !(await evaluate(this._hidden, ctx)) && (await evaluate(this._visible, ctx)) && Boolean(await evaluate(this._authorize, ctx)); }
|
|
308
307
|
visible(condition = true) { this._visible = condition; return this; }
|
|
309
|
-
/**
|
|
308
|
+
/** Shown but unusable; the server also refuses to mount it. */
|
|
310
309
|
disabled(condition = true) { this._disabled = condition; return this; }
|
|
311
310
|
async isDisabled(ctx) { return evaluate(this._disabled, ctx); }
|
|
312
|
-
/**
|
|
311
|
+
/** Renders the action as a button (or as a link when it has a URL). */
|
|
313
312
|
async toHtml(ctx, explicitMountContext = null) {
|
|
314
313
|
if (!colorManager)
|
|
315
|
-
throw new Error('
|
|
314
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
316
315
|
const linkUrl = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
|
|
317
316
|
if (linkUrl !== null && /^(\/(?!\/)|https?:\/\/)/.test(linkUrl)) {
|
|
318
317
|
const link = new Attributes().class(['fi-ac-btn-action', 'fi-btn', `fi-size-${this._size}`, ...(this._color !== 'gray' ? colorManager.getComponentClasses('button', this._color) : [])]);
|
|
@@ -322,9 +321,9 @@ export class Action {
|
|
|
322
321
|
return `<a href="${e(linkUrl)}" ${this._newTab ? 'target="_blank" ' : ''}${await this.tooltipAttribute(ctx)}${this._newTab ? '' : 'wire:navigate '}${link.toHtml()}>${linkIcon}${e((await evaluate(this._label, ctx)) ?? this.getDefaultLabel())}${badgeHtml}</a>`;
|
|
323
322
|
}
|
|
324
323
|
const isSubmit = this._submit !== null;
|
|
325
|
-
//
|
|
324
|
+
// Handler action: the click mounts the action on the server, with the host's context (e.g. the record).
|
|
326
325
|
const mountContext = explicitMountContext ?? ctx.livewire?.getActionMountContext?.() ?? {};
|
|
327
|
-
//
|
|
326
|
+
// Without context, the short form `mountAction('name')` is emitted.
|
|
328
327
|
const mountExpression = this._handler ? (Object.keys(mountContext).length ? `mountAction(${js(this.name)}, {}, ${js(mountContext)})` : `mountAction(${js(this.name)})`) : null;
|
|
329
328
|
const attributes = new Attributes()
|
|
330
329
|
.merge({
|
|
@@ -349,16 +348,16 @@ export class Action {
|
|
|
349
348
|
});
|
|
350
349
|
}
|
|
351
350
|
}
|
|
352
|
-
/**
|
|
351
|
+
/** Group of actions collapsed into a dropdown behind a "⋮" icon button. */
|
|
353
352
|
export class ActionGroup {
|
|
354
353
|
actions;
|
|
355
354
|
constructor(actions) { this.actions = actions; }
|
|
356
355
|
static make(actions) { return configured(new ActionGroup(actions)); }
|
|
357
356
|
getActions() { return this.actions; }
|
|
358
|
-
/** `keyOf`
|
|
357
|
+
/** `keyOf` returns each action's `wire:key` (it depends on the record). */
|
|
359
358
|
async toHtml(ctx, context, keyOf) {
|
|
360
359
|
if (!colorManager)
|
|
361
|
-
throw new Error('
|
|
360
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
362
361
|
const items = [];
|
|
363
362
|
for (const action of this.actions)
|
|
364
363
|
if (await action.isVisible(ctx))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/actions",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
4
|
-
"description": "Actions and modals for Navette
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
|
+
"description": "Actions and modals for Navette admin panels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"navette",
|
|
34
|
-
"filament",
|
|
35
34
|
"admin",
|
|
36
35
|
"panel",
|
|
37
36
|
"livewire",
|
|
@@ -39,8 +38,8 @@
|
|
|
39
38
|
"node"
|
|
40
39
|
],
|
|
41
40
|
"dependencies": {
|
|
42
|
-
"@navette-js/support": "1.0.0-beta.
|
|
43
|
-
"@navette-js/schemas": "1.0.0-beta.
|
|
41
|
+
"@navette-js/support": "1.0.0-beta.3",
|
|
42
|
+
"@navette-js/schemas": "1.0.0-beta.3"
|
|
44
43
|
},
|
|
45
44
|
"publishConfig": {
|
|
46
45
|
"access": "public",
|
package/views/button.ejs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Action button; submit buttons show a processing message while the form is submitting. %>
|
|
2
2
|
<button <%- keyBindings %><% if (isSubmit) { %>x-data="filamentFormButton" x-bind:class="{ 'fi-processing': isProcessing }" <% } %><%- attributes %>>
|
|
3
3
|
<% if (!iconAfter) { %><%- iconHtml %><% if (hasWireLoader) { %><%- wireLoader %><% } %><% if (isSubmit) { %><%- formLoader %><% } %><% } %>
|
|
4
4
|
<% if (isSubmit) { %>
|
package/views/modal.ejs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Modal of a mounted action (confirmation or form). Closing it unmounts the action. %>
|
|
2
2
|
<div wire:partial="action-modals.<%- index %>"<% if (description) { %> aria-describedby="<%= id %>.description"<% } %> aria-labelledby="<%= id %>.heading" aria-modal="true" id="<%= id %>" role="<%- confirmation ? 'alertdialog' : 'dialog' %>" tabindex="-1" x-data="filamentModal({
|
|
3
3
|
id: <%- js(id) %>,
|
|
4
4
|
isScrollLocked: true,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
|
-
<%#
|
|
40
|
+
<%# The six ActionsRenderHook slots are already assembled into `content` by Action.toModalHtml(). %>
|
|
41
41
|
<% if (content !== null) { %>
|
|
42
42
|
<div wire:key="<%= livewireId %>.modal.<%= id %>.content" class="fi-modal-content">
|
|
43
43
|
<%- content %>
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
<div wire:key="<%= livewireId %>.modal.<%= id %>.footer" class="fi-modal-footer fi-align-<%- confirmation ? 'center' : 'start' %>">
|
|
48
48
|
<div class="fi-modal-footer-actions">
|
|
49
|
-
<%# Confirmation
|
|
49
|
+
<%# Confirmation: cancel then confirm; form: submit then cancel. %>
|
|
50
50
|
<%- confirmation ? cancel + submit : submit + cancel %>
|
|
51
51
|
</div>
|
|
52
52
|
</div>
|