@navette-js/actions 0.0.0-stage → 1.0.0-beta.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 +21 -0
- package/README.md +8 -2
- package/lib/index.d.ts +183 -0
- package/lib/index.js +370 -0
- package/package.json +46 -4
- package/views/button.ejs +13 -0
- package/views/modal.ejs +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NavetteJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @navette-js/actions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Actions and modals for Navette admin panels.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
**Beta:** the API may still change between beta releases.
|
|
8
|
+
|
|
9
|
+
License: MIT. Third-party notices: see `THIRD-PARTY-NOTICES.md` where present.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { type ColorManager, type Lazy } from '@navette-js/support';
|
|
2
|
+
import type { ComponentContext, Renderable } from '@navette-js/schemas';
|
|
3
|
+
/** Render hook slots of an action modal. Hooks receive the action as `data.action`. */
|
|
4
|
+
export declare const ActionsRenderHook: {
|
|
5
|
+
readonly MODAL_CUSTOM_CONTENT_AFTER: "actions::modal.custom-content.after";
|
|
6
|
+
readonly MODAL_CUSTOM_CONTENT_BEFORE: "actions::modal.custom-content.before";
|
|
7
|
+
readonly MODAL_CUSTOM_CONTENT_FOOTER_AFTER: "actions::modal.custom-content-footer.after";
|
|
8
|
+
readonly MODAL_CUSTOM_CONTENT_FOOTER_BEFORE: "actions::modal.custom-content-footer.before";
|
|
9
|
+
readonly MODAL_SCHEMA_AFTER: "actions::modal.schema.after";
|
|
10
|
+
readonly MODAL_SCHEMA_BEFORE: "actions::modal.schema.before";
|
|
11
|
+
};
|
|
12
|
+
export declare const useColorManager: (manager: ColorManager) => void;
|
|
13
|
+
/** Execution context of a mounted action. */
|
|
14
|
+
export type ActionCallContext = {
|
|
15
|
+
livewire: any;
|
|
16
|
+
record: any;
|
|
17
|
+
arguments: Record<string, unknown>;
|
|
18
|
+
data: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
export declare class Action implements Renderable {
|
|
21
|
+
/** Default setup for every instance created afterwards by `make()`, e.g. `Action.configureUsing((action) => action.size('sm'))`. */
|
|
22
|
+
static configureUsing<T>(this: abstract new (...args: any[]) => T, configure: (instance: T) => void): void;
|
|
23
|
+
private name;
|
|
24
|
+
private _label;
|
|
25
|
+
private _color;
|
|
26
|
+
private _size;
|
|
27
|
+
private _submit;
|
|
28
|
+
private _hidden;
|
|
29
|
+
private _visible;
|
|
30
|
+
private _disabled;
|
|
31
|
+
private _url;
|
|
32
|
+
private _action;
|
|
33
|
+
private _handler;
|
|
34
|
+
private _requiresConfirmation;
|
|
35
|
+
private _modalHeading;
|
|
36
|
+
private _modalDescription;
|
|
37
|
+
private _modalSubmitActionLabel;
|
|
38
|
+
private _modalIcon;
|
|
39
|
+
private _icon;
|
|
40
|
+
private _alpineClick;
|
|
41
|
+
private _keyBindings;
|
|
42
|
+
private _schema;
|
|
43
|
+
private _extraModalFooterActions;
|
|
44
|
+
private _schemaColumns;
|
|
45
|
+
private _fillForm;
|
|
46
|
+
private _modalWidth;
|
|
47
|
+
private _lazyUrl;
|
|
48
|
+
private _tableIcon;
|
|
49
|
+
private _groupedIcon;
|
|
50
|
+
private _lazyColor;
|
|
51
|
+
private _lazyIcon;
|
|
52
|
+
constructor(name: string);
|
|
53
|
+
static make(name: string): Action;
|
|
54
|
+
label(label: Lazy<string | null, ComponentContext>): this;
|
|
55
|
+
/** Pass a function to make the color depend on the record (row actions). */
|
|
56
|
+
color(color: string | ((ctx: ComponentContext) => string | Promise<string>)): this;
|
|
57
|
+
size(size: string): this;
|
|
58
|
+
/** Renders a `type="submit"` button that triggers the form's submit method, e.g. `.submit('authenticate')`. */
|
|
59
|
+
submit(form: string | null): this;
|
|
60
|
+
hidden(condition?: Lazy<boolean, ComponentContext>): this;
|
|
61
|
+
/** Navigation action, rendered as `<a href wire:navigate>`. */
|
|
62
|
+
url(url: string | null | ((ctx: ComponentContext) => string | null)): this;
|
|
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
|
+
action(action: string | ((ctx: ActionCallContext) => unknown | Promise<unknown>) | null): this;
|
|
65
|
+
icon(name: string | null | ((ctx: ComponentContext) => string | null | Promise<string | null>)): this;
|
|
66
|
+
/** Icon used when the action is a table row link. */
|
|
67
|
+
tableIcon(name: string | null): this;
|
|
68
|
+
private _iconButton;
|
|
69
|
+
/** Icon-only row action; the label becomes the tooltip and accessible name. */
|
|
70
|
+
iconButton(condition?: boolean): this;
|
|
71
|
+
/** Icon used when the action is a menu item (ActionGroup). */
|
|
72
|
+
groupedIcon(name: string | null): this;
|
|
73
|
+
requiresConfirmation(condition?: boolean): this;
|
|
74
|
+
modalHeading(heading: Lazy<string | null, ActionCallContext>): this;
|
|
75
|
+
modalDescription(description: Lazy<string | null, ActionCallContext>): this;
|
|
76
|
+
modalSubmitActionLabel(label: string | null): this;
|
|
77
|
+
modalIcon(name: string | null): this;
|
|
78
|
+
private _badge;
|
|
79
|
+
private _badgeColor;
|
|
80
|
+
/** Badge (counter) shown on the button. */
|
|
81
|
+
badge(badge: string | number | null | (() => string | number | null | Promise<string | number | null>)): this;
|
|
82
|
+
badgeColor(color: string): this;
|
|
83
|
+
private _slideOver;
|
|
84
|
+
private _hasSubmit;
|
|
85
|
+
/** Opens the modal as a slide-over panel. */
|
|
86
|
+
slideOver(condition?: boolean): this;
|
|
87
|
+
/** `false`: read-only modal, without a submit button. */
|
|
88
|
+
modalSubmitAction(condition: boolean): this;
|
|
89
|
+
private _cancelLabel;
|
|
90
|
+
/** Label of the modal's close button ("Cancel" by default). */
|
|
91
|
+
modalCancelActionLabel(label: string | null): this;
|
|
92
|
+
private _tooltip;
|
|
93
|
+
private _newTab;
|
|
94
|
+
/** Tooltip shown on hover. */
|
|
95
|
+
tooltip(text: Lazy<string | null, ComponentContext>): this;
|
|
96
|
+
/** Opens the action's URL in a new tab. */
|
|
97
|
+
openUrlInNewTab(condition?: boolean): this;
|
|
98
|
+
private tooltipAttribute;
|
|
99
|
+
private _modalIconColor;
|
|
100
|
+
/** Color of the modal icon; defaults to the action's color. */
|
|
101
|
+
modalIconColor(color: string | null): this;
|
|
102
|
+
getColor(): string;
|
|
103
|
+
getLabel(ctx: ComponentContext): Promise<string>;
|
|
104
|
+
getIcon(ctx: ComponentContext): Promise<string | null>;
|
|
105
|
+
private _sort;
|
|
106
|
+
/** Position in a menu (user menu, tenant menu); negative values go before the theme switcher or the tenant list. */
|
|
107
|
+
sort(sort: number): this;
|
|
108
|
+
getSort(): number;
|
|
109
|
+
/**
|
|
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
|
+
*/
|
|
113
|
+
toMenuItemHtml(ctx: ComponentContext, options?: {
|
|
114
|
+
navigate?: boolean;
|
|
115
|
+
}): Promise<string>;
|
|
116
|
+
/** A computed URL depends on the record, so the host checks it again on mount (`resolveUrl`). */
|
|
117
|
+
isMountable(): boolean;
|
|
118
|
+
/** URL of the action in this context; an action with a URL is a link, never a mounted action. */
|
|
119
|
+
resolveUrl(ctx: ComponentContext): string | null;
|
|
120
|
+
hasModal(): boolean;
|
|
121
|
+
/** Form shown in the modal. With a function, the components are rebuilt on every request. */
|
|
122
|
+
schema(components: any[] | ((schema: any) => any), columns?: Record<string, number> | number | null): this;
|
|
123
|
+
/** Applies the action's form to the host's schema; the function returns either components or the configured schema. */
|
|
124
|
+
applySchema(schema: any): any | null;
|
|
125
|
+
/** Extra buttons in the modal footer, after the submit button. */
|
|
126
|
+
extraModalFooterActions(actions: Action[]): this;
|
|
127
|
+
/** Initial form state (e.g. from the record). */
|
|
128
|
+
fillForm(resolver: (ctx: ActionCallContext) => Record<string, unknown> | Promise<Record<string, unknown>>): this;
|
|
129
|
+
/** Modal width (`xs` … `7xl`). Defaults to `md` for a confirmation and `4xl` for a form. */
|
|
130
|
+
modalWidth(width: string | null): this;
|
|
131
|
+
hasSchema(): boolean;
|
|
132
|
+
getFormFill(ctx: ActionCallContext): Promise<Record<string, unknown> | null>;
|
|
133
|
+
private getDefaultLabel;
|
|
134
|
+
private _mutateData;
|
|
135
|
+
private _before;
|
|
136
|
+
private _after;
|
|
137
|
+
/** Transforms the validated form data before the handler runs (add the author, normalize a value…). */
|
|
138
|
+
mutateDataUsing(fn: (data: Record<string, unknown>, ctx: ActionCallContext) => Record<string, unknown> | Promise<Record<string, unknown>>): this;
|
|
139
|
+
/** Runs right before the handler; throwing cancels the action. */
|
|
140
|
+
before(fn: (ctx: ActionCallContext) => unknown | Promise<unknown>): this;
|
|
141
|
+
/** Runs after the handler succeeds (logging, extra notification…). */
|
|
142
|
+
after(fn: (ctx: ActionCallContext) => unknown | Promise<unknown>): this;
|
|
143
|
+
call(ctx: ActionCallContext): Promise<unknown>;
|
|
144
|
+
/** Menu item of a table bulk action. The click goes through the table's Alpine `mountAction()`, which sends the selection. */
|
|
145
|
+
toGroupedHtml(ctx: ComponentContext, context: Record<string, unknown>): Promise<string>;
|
|
146
|
+
/**
|
|
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
|
+
*/
|
|
150
|
+
toRecordGroupedHtml(ctx: ComponentContext, context: Record<string, unknown>, wireKey: string): Promise<string>;
|
|
151
|
+
/** Small link (or icon button) for a table row action. */
|
|
152
|
+
toLinkHtml(ctx: ComponentContext, context: Record<string, unknown>, wireKey: string): Promise<string>;
|
|
153
|
+
/** Modal of a mounted action; `content` is the rendered form, if any. */
|
|
154
|
+
toModalHtml(ctx: ActionCallContext, index: number, content?: string | null): Promise<string>;
|
|
155
|
+
alpineClickHandler(handler: string | null): this;
|
|
156
|
+
private _livewireTarget;
|
|
157
|
+
private _iconAfter;
|
|
158
|
+
/** Loading indicator target (`wire:target`) when the click is handled in JavaScript. */
|
|
159
|
+
livewireTarget(target: string | null): this;
|
|
160
|
+
/** `'after'` puts the icon (and loading indicator) after the label. */
|
|
161
|
+
iconPosition(position: 'before' | 'after'): this;
|
|
162
|
+
keyBindings(bindings: string[]): this;
|
|
163
|
+
getName(): string;
|
|
164
|
+
private _authorize;
|
|
165
|
+
/** Authorization check, separate from `visible` / `hidden`: when it fails, the action is hidden and the server refuses it. */
|
|
166
|
+
authorize(condition: Lazy<boolean, ComponentContext>): this;
|
|
167
|
+
isVisible(ctx: ComponentContext): Promise<boolean>;
|
|
168
|
+
visible(condition?: Lazy<boolean, ComponentContext>): this;
|
|
169
|
+
/** Shown but unusable; the server also refuses to mount it. */
|
|
170
|
+
disabled(condition?: Lazy<boolean, ComponentContext>): this;
|
|
171
|
+
isDisabled(ctx: ComponentContext): Promise<boolean>;
|
|
172
|
+
/** Renders the action as a button (or as a link when it has a URL). */
|
|
173
|
+
toHtml(ctx: ComponentContext, explicitMountContext?: Record<string, unknown> | null): Promise<string>;
|
|
174
|
+
}
|
|
175
|
+
/** Group of actions collapsed into a dropdown behind a "⋮" icon button. */
|
|
176
|
+
export declare class ActionGroup {
|
|
177
|
+
private actions;
|
|
178
|
+
constructor(actions: Action[]);
|
|
179
|
+
static make(actions: Action[]): ActionGroup;
|
|
180
|
+
getActions(): Action[];
|
|
181
|
+
/** `keyOf` returns each action's `wire:key` (it depends on the record). */
|
|
182
|
+
toHtml(ctx: ComponentContext, context: Record<string, unknown>, keyOf: (action: Action) => string): Promise<string>;
|
|
183
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// @navette-js/actions — actions: buttons, links, menu items and modal actions.
|
|
2
|
+
import { Attributes, e, evaluate, icon, js, loadingIndicator, renderHook, views, configured, configureUsing } from '@navette-js/support';
|
|
3
|
+
import { t } from '@navette-js/support/i18n';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
views.namespace('filament-actions', fileURLToPath(new URL('../views', import.meta.url)));
|
|
6
|
+
/** Render hook slots of an action modal. Hooks receive the action as `data.action`. */
|
|
7
|
+
export const ActionsRenderHook = {
|
|
8
|
+
MODAL_CUSTOM_CONTENT_AFTER: 'actions::modal.custom-content.after',
|
|
9
|
+
MODAL_CUSTOM_CONTENT_BEFORE: 'actions::modal.custom-content.before',
|
|
10
|
+
MODAL_CUSTOM_CONTENT_FOOTER_AFTER: 'actions::modal.custom-content-footer.after',
|
|
11
|
+
MODAL_CUSTOM_CONTENT_FOOTER_BEFORE: 'actions::modal.custom-content-footer.before',
|
|
12
|
+
MODAL_SCHEMA_AFTER: 'actions::modal.schema.after',
|
|
13
|
+
MODAL_SCHEMA_BEFORE: 'actions::modal.schema.before',
|
|
14
|
+
};
|
|
15
|
+
let colorManager = null;
|
|
16
|
+
export const useColorManager = (manager) => { colorManager = manager; };
|
|
17
|
+
export class Action {
|
|
18
|
+
/** Default setup for every instance created afterwards by `make()`, e.g. `Action.configureUsing((action) => action.size('sm'))`. */
|
|
19
|
+
static configureUsing(configure) { configureUsing(this, configure); }
|
|
20
|
+
name;
|
|
21
|
+
_label = null;
|
|
22
|
+
_color = 'primary';
|
|
23
|
+
_size = 'md';
|
|
24
|
+
_submit = null;
|
|
25
|
+
_hidden = false;
|
|
26
|
+
_visible = true;
|
|
27
|
+
_disabled = false;
|
|
28
|
+
_url = null;
|
|
29
|
+
_action = null;
|
|
30
|
+
_handler = null;
|
|
31
|
+
_requiresConfirmation = false;
|
|
32
|
+
_modalHeading = null;
|
|
33
|
+
_modalDescription = null;
|
|
34
|
+
_modalSubmitActionLabel = null;
|
|
35
|
+
_modalIcon = null;
|
|
36
|
+
_icon = null;
|
|
37
|
+
_alpineClick = null;
|
|
38
|
+
_keyBindings = [];
|
|
39
|
+
_schema = null;
|
|
40
|
+
_extraModalFooterActions = [];
|
|
41
|
+
_schemaColumns = {};
|
|
42
|
+
_fillForm = null;
|
|
43
|
+
_modalWidth = null;
|
|
44
|
+
_lazyUrl = null;
|
|
45
|
+
_tableIcon = null;
|
|
46
|
+
_groupedIcon = null;
|
|
47
|
+
_lazyColor = null;
|
|
48
|
+
_lazyIcon = null;
|
|
49
|
+
constructor(name) { this.name = name; }
|
|
50
|
+
static make(name) { return configured(new Action(name)); }
|
|
51
|
+
label(label) { this._label = label; return this; }
|
|
52
|
+
/** Pass a function to make the color depend on the record (row actions). */
|
|
53
|
+
color(color) { if (typeof color === 'function')
|
|
54
|
+
this._lazyColor = color;
|
|
55
|
+
else {
|
|
56
|
+
this._color = color;
|
|
57
|
+
this._lazyColor = null;
|
|
58
|
+
} return this; }
|
|
59
|
+
size(size) { this._size = size; return this; }
|
|
60
|
+
/** Renders a `type="submit"` button that triggers the form's submit method, e.g. `.submit('authenticate')`. */
|
|
61
|
+
submit(form) { this._submit = form; return this; }
|
|
62
|
+
hidden(condition = true) { this._hidden = condition; return this; }
|
|
63
|
+
/** Navigation action, rendered as `<a href wire:navigate>`. */
|
|
64
|
+
url(url) { if (typeof url === 'function')
|
|
65
|
+
this._lazyUrl = url;
|
|
66
|
+
else {
|
|
67
|
+
this._url = url;
|
|
68
|
+
this._lazyUrl = null;
|
|
69
|
+
} return this; }
|
|
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
|
+
action(action) {
|
|
72
|
+
if (typeof action === 'function') {
|
|
73
|
+
this._handler = action;
|
|
74
|
+
this._action = null;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this._action = action;
|
|
78
|
+
this._handler = null;
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
icon(name) { if (typeof name === 'function')
|
|
83
|
+
this._lazyIcon = name;
|
|
84
|
+
else {
|
|
85
|
+
this._icon = name;
|
|
86
|
+
this._lazyIcon = null;
|
|
87
|
+
} return this; }
|
|
88
|
+
/** Icon used when the action is a table row link. */
|
|
89
|
+
tableIcon(name) { this._tableIcon = name; return this; }
|
|
90
|
+
_iconButton = false;
|
|
91
|
+
/** Icon-only row action; the label becomes the tooltip and accessible name. */
|
|
92
|
+
iconButton(condition = true) { this._iconButton = condition; return this; }
|
|
93
|
+
/** Icon used when the action is a menu item (ActionGroup). */
|
|
94
|
+
groupedIcon(name) { this._groupedIcon = name; return this; }
|
|
95
|
+
requiresConfirmation(condition = true) { this._requiresConfirmation = condition; return this; }
|
|
96
|
+
modalHeading(heading) { this._modalHeading = heading; return this; }
|
|
97
|
+
modalDescription(description) { this._modalDescription = description; return this; }
|
|
98
|
+
modalSubmitActionLabel(label) { this._modalSubmitActionLabel = label; return this; }
|
|
99
|
+
modalIcon(name) { this._modalIcon = name; return this; }
|
|
100
|
+
_badge = null;
|
|
101
|
+
_badgeColor = 'primary';
|
|
102
|
+
/** Badge (counter) shown on the button. */
|
|
103
|
+
badge(badge) { this._badge = badge; return this; }
|
|
104
|
+
badgeColor(color) { this._badgeColor = color; return this; }
|
|
105
|
+
_slideOver = false;
|
|
106
|
+
_hasSubmit = true;
|
|
107
|
+
/** Opens the modal as a slide-over panel. */
|
|
108
|
+
slideOver(condition = true) { this._slideOver = condition; return this; }
|
|
109
|
+
/** `false`: read-only modal, without a submit button. */
|
|
110
|
+
modalSubmitAction(condition) { this._hasSubmit = condition; return this; }
|
|
111
|
+
_cancelLabel = null;
|
|
112
|
+
/** Label of the modal's close button ("Cancel" by default). */
|
|
113
|
+
modalCancelActionLabel(label) { this._cancelLabel = label; return this; }
|
|
114
|
+
_tooltip = null;
|
|
115
|
+
_newTab = false;
|
|
116
|
+
/** Tooltip shown on hover. */
|
|
117
|
+
tooltip(text) { this._tooltip = text; return this; }
|
|
118
|
+
/** Opens the action's URL in a new tab. */
|
|
119
|
+
openUrlInNewTab(condition = true) { this._newTab = condition; return this; }
|
|
120
|
+
async tooltipAttribute(ctx) {
|
|
121
|
+
const text = await evaluate(this._tooltip, ctx);
|
|
122
|
+
return text ? `x-tooltip="${e(`{
|
|
123
|
+
content: ${js(text)},
|
|
124
|
+
theme: $store.theme,
|
|
125
|
+
allowHTML: false,
|
|
126
|
+
}`)}" ` : '';
|
|
127
|
+
}
|
|
128
|
+
_modalIconColor = null;
|
|
129
|
+
/** Color of the modal icon; defaults to the action's color. */
|
|
130
|
+
modalIconColor(color) { this._modalIconColor = color; return this; }
|
|
131
|
+
getColor() { return this._color; }
|
|
132
|
+
async getLabel(ctx) { return (await evaluate(this._label, ctx)) ?? this.getDefaultLabel(); }
|
|
133
|
+
async getIcon(ctx) { return this._lazyIcon ? this._lazyIcon(ctx) : this._icon; }
|
|
134
|
+
_sort = 0;
|
|
135
|
+
/** Position in a menu (user menu, tenant menu); negative values go before the theme switcher or the tenant list. */
|
|
136
|
+
sort(sort) { this._sort = sort; return this; }
|
|
137
|
+
getSort() { return this._sort; }
|
|
138
|
+
/**
|
|
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
|
+
*/
|
|
142
|
+
async toMenuItemHtml(ctx, options = {}) {
|
|
143
|
+
if (!colorManager)
|
|
144
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
145
|
+
const href = this.resolveUrl(ctx);
|
|
146
|
+
if (!href || !/^(\/(?!\/)|https?:\/\/)/.test(href))
|
|
147
|
+
return '';
|
|
148
|
+
const iconName = await this.getIcon(ctx);
|
|
149
|
+
const colors = this._color !== 'gray' && this._color !== 'primary' ? colorManager.getComponentClasses('dropdown-item', this._color) : [];
|
|
150
|
+
const glyph = iconName ? icon(iconName, { attributes: new Attributes().class(colors.length ? ['fi-color', `fi-color-${this._color}`] : []) }) : '';
|
|
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
|
+
}
|
|
153
|
+
/** A computed URL depends on the record, so the host checks it again on mount (`resolveUrl`). */
|
|
154
|
+
isMountable() { return (this._handler !== null || this._schema !== null) && this._url === null; }
|
|
155
|
+
/** URL of the action in this context; an action with a URL is a link, never a mounted action. */
|
|
156
|
+
resolveUrl(ctx) { return this._lazyUrl ? this._lazyUrl(ctx) : this._url; }
|
|
157
|
+
hasModal() { return this._requiresConfirmation || this._schema !== null; }
|
|
158
|
+
/** Form shown in the modal. With a function, the components are rebuilt on every request. */
|
|
159
|
+
schema(components, columns = {}) { this._schema = typeof components === 'function' ? components : () => components; this._schemaColumns = columns; return this; }
|
|
160
|
+
/** Applies the action's form to the host's schema; the function returns either components or the configured schema. */
|
|
161
|
+
applySchema(schema) {
|
|
162
|
+
if (!this._schema)
|
|
163
|
+
return null;
|
|
164
|
+
const result = this._schema(schema);
|
|
165
|
+
return Array.isArray(result) ? schema.columns(this._schemaColumns).components(result) : result;
|
|
166
|
+
}
|
|
167
|
+
/** Extra buttons in the modal footer, after the submit button. */
|
|
168
|
+
extraModalFooterActions(actions) { this._extraModalFooterActions = actions; return this; }
|
|
169
|
+
/** Initial form state (e.g. from the record). */
|
|
170
|
+
fillForm(resolver) { this._fillForm = resolver; return this; }
|
|
171
|
+
/** Modal width (`xs` … `7xl`). Defaults to `md` for a confirmation and `4xl` for a form. */
|
|
172
|
+
modalWidth(width) { this._modalWidth = width; return this; }
|
|
173
|
+
hasSchema() { return this._schema !== null; }
|
|
174
|
+
async getFormFill(ctx) { return this._fillForm ? this._fillForm(ctx) : null; }
|
|
175
|
+
getDefaultLabel() { return ((s) => s.charAt(0).toUpperCase() + s.slice(1))(this.name.replace(/[_-]+/g, ' ')); }
|
|
176
|
+
_mutateData = null;
|
|
177
|
+
_before = null;
|
|
178
|
+
_after = null;
|
|
179
|
+
/** Transforms the validated form data before the handler runs (add the author, normalize a value…). */
|
|
180
|
+
mutateDataUsing(fn) { this._mutateData = fn; return this; }
|
|
181
|
+
/** Runs right before the handler; throwing cancels the action. */
|
|
182
|
+
before(fn) { this._before = fn; return this; }
|
|
183
|
+
/** Runs after the handler succeeds (logging, extra notification…). */
|
|
184
|
+
after(fn) { this._after = fn; return this; }
|
|
185
|
+
async call(ctx) {
|
|
186
|
+
if (!this._handler)
|
|
187
|
+
return null;
|
|
188
|
+
if (this._mutateData)
|
|
189
|
+
ctx = { ...ctx, data: await this._mutateData(ctx.data ?? {}, ctx) };
|
|
190
|
+
await this._before?.(ctx);
|
|
191
|
+
const result = await this._handler(ctx);
|
|
192
|
+
await this._after?.(ctx);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/** Menu item of a table bulk action. The click goes through the table's Alpine `mountAction()`, which sends the selection. */
|
|
196
|
+
async toGroupedHtml(ctx, context) {
|
|
197
|
+
if (!colorManager)
|
|
198
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
199
|
+
const expression = `mountAction(${js(this.name)}, {}, ${js(context)})`;
|
|
200
|
+
const attributes = new Attributes()
|
|
201
|
+
.merge({ role: 'menuitem', tabindex: '-1', type: 'button', 'wire:loading.attr': 'aria-disabled', 'wire:loading.class': 'fi-disabled', 'wire:target': expression })
|
|
202
|
+
.class(['fi-ac-grouped-action', 'fi-dropdown-list-item', ...(this._color !== 'gray' ? colorManager.getComponentClasses('dropdown-item', this._color) : [])])
|
|
203
|
+
.set('x-cloak', 'x-cloak').set('x-show', 'getSelectedRecordsCount()').set('x-on:click', expression);
|
|
204
|
+
const iconHtml = this._icon ? icon(this._icon, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': expression }).class(this._color !== 'gray' ? ['fi-color', `fi-color-${this._color}`] : []) }) : '';
|
|
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
|
+
}
|
|
207
|
+
/**
|
|
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
|
+
*/
|
|
211
|
+
async toRecordGroupedHtml(ctx, context, wireKey) {
|
|
212
|
+
if (!colorManager)
|
|
213
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
214
|
+
const color = this._lazyColor ? await this._lazyColor(ctx) : this._color;
|
|
215
|
+
const iconName = this._lazyIcon ? await this._lazyIcon(ctx) : (this._groupedIcon ?? this._icon);
|
|
216
|
+
const expression = e(`mountAction(${js(this.name)}, {}, ${js(context)})`);
|
|
217
|
+
const colors = color !== 'gray' ? colorManager.getComponentClasses('dropdown-item', color) : [];
|
|
218
|
+
const label = (await evaluate(this._label, ctx)) ?? this.getDefaultLabel();
|
|
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
|
+
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
|
+
// Navigation action: a plain link, nothing is mounted on the server.
|
|
222
|
+
const href = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
|
|
223
|
+
if (href) {
|
|
224
|
+
const plainIcon = iconName ? icon(iconName, { attributes: new Attributes().class(color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []) }) : '';
|
|
225
|
+
// New-tab link: no `wire:navigate`.
|
|
226
|
+
if (this._newTab)
|
|
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
|
+
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
|
+
}
|
|
230
|
+
if (this._disabled !== false) {
|
|
231
|
+
// Conditionally disabled: this markup is used whether or not the action ends up disabled.
|
|
232
|
+
const disabled = await this.isDisabled(ctx);
|
|
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
|
+
}
|
|
235
|
+
if (this._keyBindings.length) {
|
|
236
|
+
return `<button x-bind:id="$id('key-bindings')" x-mousetrap.global.${this._keyBindings.map((k) => k.replace(/\+/g, '-')).join('.')}="document.getElementById($el.id)?.click()" 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', ...colors].join(' ')}" wire:click="${expression}" wire:key="${e(wireKey)}">${inner}</button>`;
|
|
237
|
+
}
|
|
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
|
+
}
|
|
240
|
+
/** Small link (or icon button) for a table row action. */
|
|
241
|
+
async toLinkHtml(ctx, context, wireKey) {
|
|
242
|
+
if (!colorManager)
|
|
243
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
244
|
+
const color = this._lazyColor ? await this._lazyColor(ctx) : this._color;
|
|
245
|
+
const iconName = this._lazyIcon ? await this._lazyIcon(ctx) : (this._tableIcon ?? this._icon);
|
|
246
|
+
const raw = `mountAction(${js(this.name)}, {}, ${js(context)})`;
|
|
247
|
+
const label = (await evaluate(this._label, ctx)) ?? this.getDefaultLabel();
|
|
248
|
+
const classes = ['fi-ac-link-action', 'fi-link', 'fi-size-sm', ...(color !== 'gray' ? colorManager.getComponentClasses('link', color) : [])].join(' ');
|
|
249
|
+
if (this._iconButton) {
|
|
250
|
+
const disabled = this._disabled !== false && (await this.isDisabled(ctx));
|
|
251
|
+
const buttonClasses = ['fi-ac-icon-btn-action', 'fi-icon-btn', 'fi-size-sm', disabled ? 'fi-disabled' : null, ...(color !== 'gray' ? colorManager.getComponentClasses('icon-button', color) : [])].filter(Boolean).join(' ');
|
|
252
|
+
const glyph = iconName ? icon(iconName, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': raw }) }) : '';
|
|
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
|
+
}
|
|
255
|
+
const iconHtml = iconName ? icon(iconName, { size: 'sm', attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': raw }) }) : '';
|
|
256
|
+
const href = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
|
|
257
|
+
const tooltip = await this.tooltipAttribute(ctx);
|
|
258
|
+
// Only local or http(s) URLs become links (no `javascript:` or protocol-relative URLs).
|
|
259
|
+
if (href && /^(\/(?!\/)|https?:\/\/)/.test(href))
|
|
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>`;
|
|
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()" ` : '';
|
|
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>`;
|
|
263
|
+
}
|
|
264
|
+
/** Modal of a mounted action; `content` is the rendered form, if any. */
|
|
265
|
+
async toModalHtml(ctx, index, content = null) {
|
|
266
|
+
if (!colorManager)
|
|
267
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
268
|
+
const schemaCtx = { livewire: ctx.livewire, component: null, operation: null, record: ctx.record, get: () => undefined, state: undefined };
|
|
269
|
+
const label = (await evaluate(this._label, schemaCtx)) ?? this.getDefaultLabel();
|
|
270
|
+
const color = this._lazyColor ? await this._lazyColor(schemaCtx) : this._color;
|
|
271
|
+
const confirmation = this._requiresConfirmation;
|
|
272
|
+
// A confirmation is centered (md); a form is start-aligned (4xl), except at small widths.
|
|
273
|
+
const width = this._modalWidth ?? (confirmation ? 'md' : '4xl');
|
|
274
|
+
const submitLabel = this._modalSubmitActionLabel ?? t(confirmation ? 'filament-actions::modal.actions.confirm.label' : 'filament-actions::modal.actions.submit.label');
|
|
275
|
+
// Hook order: custom content, schema, content footer, each between its before/after slots.
|
|
276
|
+
const scopes = ctx.livewire.getRenderHookScopes?.() ?? [];
|
|
277
|
+
const hook = (name) => renderHook(name, scopes, { action: this });
|
|
278
|
+
const hooked = hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_BEFORE) + hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_AFTER)
|
|
279
|
+
+ (content !== null ? hook(ActionsRenderHook.MODAL_SCHEMA_BEFORE) + content + hook(ActionsRenderHook.MODAL_SCHEMA_AFTER) : '')
|
|
280
|
+
+ hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_FOOTER_BEFORE) + hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_FOOTER_AFTER);
|
|
281
|
+
if (content !== null || hooked !== '')
|
|
282
|
+
content = hooked;
|
|
283
|
+
return views.render('filament-actions::modal', {
|
|
284
|
+
slideOver: this._slideOver, content, confirmation, width, alignment: confirmation || ['xs', 'sm'].includes(width) ? 'center' : 'start',
|
|
285
|
+
livewireId: ctx.livewire.id, id: `fi-${ctx.livewire.id}-action-${index}`, index, name: this.name,
|
|
286
|
+
heading: (await evaluate(this._modalHeading, ctx)) ?? label,
|
|
287
|
+
description: (await evaluate(this._modalDescription, ctx)) ?? (this._requiresConfirmation ? t('filament-actions::modal.confirmation') : null),
|
|
288
|
+
iconName: this._modalIcon ?? (this._requiresConfirmation ? 'o-exclamation-triangle' : null), color,
|
|
289
|
+
cancel: await Action.make('cancel').label(this._cancelLabel ?? t('filament-actions::modal.actions.cancel.label')).color('gray').alpineClickHandler('close()').toHtml(schemaCtx),
|
|
290
|
+
// The submit button of a gray action uses the primary color.
|
|
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(''),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
alpineClickHandler(handler) { this._alpineClick = handler; return this; }
|
|
295
|
+
_livewireTarget = null;
|
|
296
|
+
_iconAfter = false;
|
|
297
|
+
/** Loading indicator target (`wire:target`) when the click is handled in JavaScript. */
|
|
298
|
+
livewireTarget(target) { this._livewireTarget = target; return this; }
|
|
299
|
+
/** `'after'` puts the icon (and loading indicator) after the label. */
|
|
300
|
+
iconPosition(position) { this._iconAfter = position === 'after'; return this; }
|
|
301
|
+
keyBindings(bindings) { this._keyBindings = bindings; return this; }
|
|
302
|
+
getName() { return this.name; }
|
|
303
|
+
_authorize = true;
|
|
304
|
+
/** Authorization check, separate from `visible` / `hidden`: when it fails, the action is hidden and the server refuses it. */
|
|
305
|
+
authorize(condition) { this._authorize = condition; return this; }
|
|
306
|
+
async isVisible(ctx) { return !(await evaluate(this._hidden, ctx)) && (await evaluate(this._visible, ctx)) && Boolean(await evaluate(this._authorize, ctx)); }
|
|
307
|
+
visible(condition = true) { this._visible = condition; return this; }
|
|
308
|
+
/** Shown but unusable; the server also refuses to mount it. */
|
|
309
|
+
disabled(condition = true) { this._disabled = condition; return this; }
|
|
310
|
+
async isDisabled(ctx) { return evaluate(this._disabled, ctx); }
|
|
311
|
+
/** Renders the action as a button (or as a link when it has a URL). */
|
|
312
|
+
async toHtml(ctx, explicitMountContext = null) {
|
|
313
|
+
if (!colorManager)
|
|
314
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
315
|
+
const linkUrl = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
|
|
316
|
+
if (linkUrl !== null && /^(\/(?!\/)|https?:\/\/)/.test(linkUrl)) {
|
|
317
|
+
const link = new Attributes().class(['fi-ac-btn-action', 'fi-btn', `fi-size-${this._size}`, ...(this._color !== 'gray' ? colorManager.getComponentClasses('button', this._color) : [])]);
|
|
318
|
+
const linkIcon = this._icon ? icon(this._icon) : '';
|
|
319
|
+
const badge = typeof this._badge === 'function' ? await this._badge() : this._badge;
|
|
320
|
+
const badgeHtml = badge !== null && badge !== '' ? `<div class="fi-btn-badge-ctn"><span class="${['fi-color', `fi-color-${this._badgeColor}`, ...colorManager.getComponentClasses('badge', this._badgeColor).filter((c) => !c.startsWith('fi-color')), 'fi-badge', 'fi-size-xs'].join(' ')}">${e(String(badge))}</span></div>` : '';
|
|
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>`;
|
|
322
|
+
}
|
|
323
|
+
const isSubmit = this._submit !== null;
|
|
324
|
+
// Handler action: the click mounts the action on the server, with the host's context (e.g. the record).
|
|
325
|
+
const mountContext = explicitMountContext ?? ctx.livewire?.getActionMountContext?.() ?? {};
|
|
326
|
+
// Without context, the short form `mountAction('name')` is emitted.
|
|
327
|
+
const mountExpression = this._handler ? (Object.keys(mountContext).length ? `mountAction(${js(this.name)}, {}, ${js(mountContext)})` : `mountAction(${js(this.name)})`) : null;
|
|
328
|
+
const attributes = new Attributes()
|
|
329
|
+
.merge({
|
|
330
|
+
type: isSubmit ? 'submit' : 'button',
|
|
331
|
+
'wire:loading.attr': 'aria-disabled',
|
|
332
|
+
'wire:loading.class': 'fi-disabled',
|
|
333
|
+
'wire:target': this._livewireTarget ?? this._submit,
|
|
334
|
+
'x-bind:disabled': isSubmit ? 'isProcessing' : null,
|
|
335
|
+
})
|
|
336
|
+
.class(['fi-ac-btn-action', 'fi-btn', `fi-size-${this._size}`, ...colorManager.getComponentClasses('button', this._color)])
|
|
337
|
+
.when(this._action, (a) => a.set('wire:click', this._action))
|
|
338
|
+
.when(mountExpression, (a) => a.set('wire:click', mountExpression))
|
|
339
|
+
.when(this._alpineClick, (a) => a.set('x-on:click', this._alpineClick));
|
|
340
|
+
const wireTarget = this._livewireTarget ?? this._submit ?? this._action ?? mountExpression;
|
|
341
|
+
const keyBindings = 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()" ` : '';
|
|
342
|
+
return views.render('filament-actions::button', {
|
|
343
|
+
attributes: attributes.toHtml(), isSubmit, keyBindings, iconAfter: this._iconAfter, hasWireLoader: wireTarget !== null,
|
|
344
|
+
label: (await evaluate(this._label, ctx)) ?? this.getDefaultLabel(),
|
|
345
|
+
wireLoader: loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': wireTarget })),
|
|
346
|
+
iconHtml: this._icon ? icon(this._icon, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': wireTarget }) }) : '',
|
|
347
|
+
formLoader: loadingIndicator(new Attributes({ 'x-cloak': 'x-cloak', 'x-show': 'isProcessing' })),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** Group of actions collapsed into a dropdown behind a "⋮" icon button. */
|
|
352
|
+
export class ActionGroup {
|
|
353
|
+
actions;
|
|
354
|
+
constructor(actions) { this.actions = actions; }
|
|
355
|
+
static make(actions) { return configured(new ActionGroup(actions)); }
|
|
356
|
+
getActions() { return this.actions; }
|
|
357
|
+
/** `keyOf` returns each action's `wire:key` (it depends on the record). */
|
|
358
|
+
async toHtml(ctx, context, keyOf) {
|
|
359
|
+
if (!colorManager)
|
|
360
|
+
throw new Error('No color manager: call useColorManager() first.');
|
|
361
|
+
const items = [];
|
|
362
|
+
for (const action of this.actions)
|
|
363
|
+
if (await action.isVisible(ctx))
|
|
364
|
+
items.push(await action.toRecordGroupedHtml(ctx, context, keyOf(action)));
|
|
365
|
+
if (!items.length)
|
|
366
|
+
return '';
|
|
367
|
+
const label = e(t('filament-actions::group.trigger.label'));
|
|
368
|
+
return `<div x-data="filamentDropdown" class="fi-dropdown"><div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger"><button title="${label}" aria-label="${label}" type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${['fi-ac-icon-btn-group', 'fi-icon-btn', 'fi-size-md', ...colorManager.getComponentClasses('icon-button', 'primary')].join(' ')}">${icon('m-ellipsis-vertical')}</button></div><div x-cloak x-float.placement.bottom-end.flip.teleport.offset="{ offset: 8 }" x-ref="panel" x-transition:enter-start="fi-opacity-0" x-transition:leave-end="fi-opacity-0" role="menu" class="fi-dropdown-panel"><div class="fi-dropdown-list" role="group">${items.join(' ')}</div></div></div>`;
|
|
369
|
+
}
|
|
370
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/actions",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
|
+
"description": "Actions and modals for Navette admin panels.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"default": "./lib/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"views",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/NavetteJS/navette.git",
|
|
26
|
+
"directory": "packages/actions"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/NavetteJS/navette#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/NavetteJS/navette/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"navette",
|
|
34
|
+
"admin",
|
|
35
|
+
"panel",
|
|
36
|
+
"livewire",
|
|
37
|
+
"express",
|
|
38
|
+
"node"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@navette-js/support": "1.0.0-beta.2",
|
|
42
|
+
"@navette-js/schemas": "1.0.0-beta.2"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"tag": "beta"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/views/button.ejs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%# Action button; submit buttons show a processing message while the form is submitting. %>
|
|
2
|
+
<button <%- keyBindings %><% if (isSubmit) { %>x-data="filamentFormButton" x-bind:class="{ 'fi-processing': isProcessing }" <% } %><%- attributes %>>
|
|
3
|
+
<% if (!iconAfter) { %><%- iconHtml %><% if (hasWireLoader) { %><%- wireLoader %><% } %><% if (isSubmit) { %><%- formLoader %><% } %><% } %>
|
|
4
|
+
<% if (isSubmit) { %>
|
|
5
|
+
<span x-show="! isProcessing">
|
|
6
|
+
<%= label %>
|
|
7
|
+
</span>
|
|
8
|
+
<span x-cloak x-show="isProcessing" x-text="processingMessage"></span>
|
|
9
|
+
<% } else { %>
|
|
10
|
+
<%= label %>
|
|
11
|
+
<% } %>
|
|
12
|
+
<% if (iconAfter) { %><%- iconHtml %><% if (hasWireLoader) { %><%- wireLoader %><% } %><% if (isSubmit) { %><%- formLoader %><% } %><% } %>
|
|
13
|
+
</button>
|
package/views/modal.ejs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%# Modal of a mounted action (confirmation or form). Closing it unmounts the action. %>
|
|
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
|
+
id: <%- js(id) %>,
|
|
4
|
+
isScrollLocked: true,
|
|
5
|
+
shouldRestoreFocus: false,
|
|
6
|
+
})" data-fi-modal-id="<%= id %>" x-on:close-modal.window="if (($event.detail.id === <%- js(id) %>) && isOpen) close()" x-on:close-modal-quietly.window="if (($event.detail.id === <%- js(id) %>) && isOpen) closeQuietly()" x-on:open-modal.window="if (($event.detail.id === <%- js(id) %>) && (! isOpen)) open()" x-bind:class="{
|
|
7
|
+
'fi-modal-open': isOpen,
|
|
8
|
+
}" x-cloak x-show="isOpen" class="<%- locals.slideOver ? 'fi-modal fi-modal-slide-over fi-absolute-positioning-context' : 'fi-modal fi-absolute-positioning-context' %>" wire:key="<%= livewireId %>.actions.<%= name %>.modal" x-on:modal-closed="if ($event.detail.id === <%= js(id) %>) $wire.unmountAction(false)">
|
|
9
|
+
<div aria-hidden="true" x-show="isOpen" x-transition.duration.300ms.opacity class="fi-modal-close-overlay"></div>
|
|
10
|
+
|
|
11
|
+
<div tabindex="-1" x-on:click.self="$dispatch(<%= js('close-modal') %>, { id: <%= js(id) %> })" x-trap.noreturn="isTrapActive" class="fi-modal-window-ctn fi-clickable">
|
|
12
|
+
<form x-on:keydown.window.escape="if (isTopmost()) $dispatch(<%= js('close-modal') %>, { id: <%= js(id) %> })" x-show="isWindowVisible" x-transition:enter="fi-transition-enter" x-transition:leave="fi-transition-leave" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end" wire:submit.prevent="callMountedAction" wire:key="<%= livewireId %>.modal.<%= id %>.window" class="<%- cls(['fi-modal-window', 'fi-modal-window-has-close-btn', { 'fi-modal-window-has-content': content !== null }, 'fi-modal-window-has-footer', { 'fi-modal-window-has-icon': iconName }, 'fi-align-' + alignment, 'fi-width-' + width]) %>">
|
|
13
|
+
<div wire:key="<%= livewireId %>.modal.<%= id %>.header" class="fi-modal-header">
|
|
14
|
+
<%- view('filament::components.icon-button', {
|
|
15
|
+
iconName: 'o-x-mark', iconSize: 'lg', label: t('filament::components/modal.actions.close.label'), className: 'fi-modal-close-btn',
|
|
16
|
+
extra: attrs({ tabindex: '-1' }), after: attrs({ 'x-on:click': "$dispatch('close-modal', { id: " + js(id) + ' })' }),
|
|
17
|
+
}) %>
|
|
18
|
+
|
|
19
|
+
<% if (iconName) { %>
|
|
20
|
+
<div class="fi-modal-icon-ctn">
|
|
21
|
+
<div class="<%- cls([...(color !== 'gray' ? ['fi-color', 'fi-color-' + color] : []), 'fi-modal-icon-bg']) %>">
|
|
22
|
+
<%- icon(iconName, { size: 'lg' }) %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<% } %>
|
|
26
|
+
|
|
27
|
+
<div>
|
|
28
|
+
<h2 id="<%= id %>.heading" class="fi-modal-heading">
|
|
29
|
+
<%= heading %>
|
|
30
|
+
</h2>
|
|
31
|
+
|
|
32
|
+
<% if (description) { %>
|
|
33
|
+
<p id="<%= id %>.description" class="fi-modal-description">
|
|
34
|
+
<%= description %>
|
|
35
|
+
</p>
|
|
36
|
+
<% } %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<%# The six ActionsRenderHook slots are already assembled into `content` by Action.toModalHtml(). %>
|
|
41
|
+
<% if (content !== null) { %>
|
|
42
|
+
<div wire:key="<%= livewireId %>.modal.<%= id %>.content" class="fi-modal-content">
|
|
43
|
+
<%- content %>
|
|
44
|
+
</div>
|
|
45
|
+
<% } %>
|
|
46
|
+
|
|
47
|
+
<div wire:key="<%= livewireId %>.modal.<%= id %>.footer" class="fi-modal-footer fi-align-<%- confirmation ? 'center' : 'start' %>">
|
|
48
|
+
<div class="fi-modal-footer-actions">
|
|
49
|
+
<%# Confirmation: cancel then confirm; form: submit then cancel. %>
|
|
50
|
+
<%- confirmation ? cancel + submit : submit + cancel %>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</form>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|