@memberjunction/ng-entity-action-ux 0.0.1 → 5.43.0
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 +175 -43
- package/dist/lib/ai-prompt-selector/ai-prompt-selector.component.d.ts +41 -0
- package/dist/lib/ai-prompt-selector/ai-prompt-selector.component.d.ts.map +1 -0
- package/dist/lib/ai-prompt-selector/ai-prompt-selector.component.js +231 -0
- package/dist/lib/ai-prompt-selector/ai-prompt-selector.component.js.map +1 -0
- package/dist/lib/base-entity-action-runtime-ux.d.ts +31 -0
- package/dist/lib/base-entity-action-runtime-ux.d.ts.map +1 -0
- package/dist/lib/base-entity-action-runtime-ux.js +34 -0
- package/dist/lib/base-entity-action-runtime-ux.js.map +1 -0
- package/dist/lib/entity-action-ux-host.component.d.ts +32 -0
- package/dist/lib/entity-action-ux-host.component.d.ts.map +1 -0
- package/dist/lib/entity-action-ux-host.component.js +89 -0
- package/dist/lib/entity-action-ux-host.component.js.map +1 -0
- package/dist/lib/field-rules-builder/field-rules-builder.component.d.ts +62 -0
- package/dist/lib/field-rules-builder/field-rules-builder.component.d.ts.map +1 -0
- package/dist/lib/field-rules-builder/field-rules-builder.component.js +411 -0
- package/dist/lib/field-rules-builder/field-rules-builder.component.js.map +1 -0
- package/dist/lib/field-rules-model.d.ts +32 -0
- package/dist/lib/field-rules-model.d.ts.map +1 -0
- package/dist/lib/field-rules-model.js +76 -0
- package/dist/lib/field-rules-model.js.map +1 -0
- package/dist/lib/prompt-grouping.d.ts +20 -0
- package/dist/lib/prompt-grouping.d.ts.map +1 -0
- package/dist/lib/prompt-grouping.js +30 -0
- package/dist/lib/prompt-grouping.js.map +1 -0
- package/dist/lib/record-process-runner/record-process-runner-ux.component.d.ts +76 -0
- package/dist/lib/record-process-runner/record-process-runner-ux.component.d.ts.map +1 -0
- package/dist/lib/record-process-runner/record-process-runner-ux.component.js +511 -0
- package/dist/lib/record-process-runner/record-process-runner-ux.component.js.map +1 -0
- package/dist/lib/runtime-ux-context.d.ts +52 -0
- package/dist/lib/runtime-ux-context.d.ts.map +1 -0
- package/dist/lib/runtime-ux-context.js +2 -0
- package/dist/lib/runtime-ux-context.js.map +1 -0
- package/dist/lib/scope.d.ts +12 -0
- package/dist/lib/scope.d.ts.map +1 -0
- package/dist/lib/scope.js +25 -0
- package/dist/lib/scope.js.map +1 -0
- package/dist/public-api.d.ts +15 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +27 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +47 -6
package/README.md
CHANGED
|
@@ -1,45 +1,177 @@
|
|
|
1
1
|
# @memberjunction/ng-entity-action-ux
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
A **pluggable runtime-UX framework for entity actions**, plus the flagship driver: a safe, two-step
|
|
4
|
+
**Record Process bulk-update runner** (dry-run → review the exact per-record diff → confirm → apply).
|
|
5
|
+
|
|
6
|
+
Most entity actions "fire and forget." Some need a real interaction first — a preview, a confirmation, a
|
|
7
|
+
small form. This package lets an entity action declare a **`RuntimeUXDriverClass`**; when present, the host
|
|
8
|
+
mounts that interactive component instead of invoking the action blind. New drivers register via the MJ
|
|
9
|
+
ClassFactory and need **zero host changes**.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─ entity grid / list / form ─────────────────────────────────────────────┐
|
|
13
|
+
│ user clicks an entity action │
|
|
14
|
+
│ │ EntityActionRequested { action, selectedRecords, ... } │
|
|
15
|
+
│ ▼ │
|
|
16
|
+
│ parent: does action.runtimeUXDriverClass exist? │
|
|
17
|
+
│ ├─ no → invoke the action normally (existing path) │
|
|
18
|
+
│ └─ yes → <mj-entity-action-ux-host [DriverClass] [Context]> │
|
|
19
|
+
│ │ ClassFactory.GetRegistration(Base…, DriverClass) │
|
|
20
|
+
│ ▼ │
|
|
21
|
+
│ RecordProcessRunnerUX (or any registered driver) │
|
|
22
|
+
│ dry-run → diff → confirm → apply → Completed │
|
|
23
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What's in the box
|
|
27
|
+
|
|
28
|
+
| Export | Kind | Purpose |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `BaseEntityActionRuntimeUX` | `@Directive()` base | The driver contract: `@Input() Context`, `@Output() Completed/Cancelled`, `abstract Start()`. |
|
|
31
|
+
| `EntityActionUXHostComponent` | `<mj-entity-action-ux-host>` | Resolves a driver key via ClassFactory and mounts it with a context. |
|
|
32
|
+
| `EntityActionUXContext` / `EntityActionUXResult` | types | The data the host hands a driver / the outcome it returns. |
|
|
33
|
+
| `RecordProcessRunnerUXComponent` | driver (`RecordProcessRunnerUX`) | The flagship: runs a `MJ: Record Processes` as a dry-run, shows the diff, applies on confirm. |
|
|
34
|
+
| `AIPromptSelectorComponent` | `<mj-ai-prompt-selector>` | Category-grouped, searchable AI-prompt picker. |
|
|
35
|
+
| `FieldRulesBuilderComponent` | `<mj-field-rules-builder>` | Visual authoring for a `FieldRuleSet` (static / field / formula / lookup / prompt + condition), with live metadata validation. |
|
|
36
|
+
| `field-rules-model` / `scope` / `prompt-grouping` | pure fns | Angular-free, unit-tested logic the components delegate to. |
|
|
37
|
+
|
|
38
|
+
## The Record Process Runner (flagship driver)
|
|
39
|
+
|
|
40
|
+
The runner turns a **FieldRules Record Process** into a safe bulk update:
|
|
41
|
+
|
|
42
|
+
1. **Dry-run** — calls the server (`RunRecordProcess`, `DryRun: true`), which computes the exact per-record
|
|
43
|
+
field changes **without writing**, and persists them to `MJ: Process Run Details`.
|
|
44
|
+
2. **Review** — reads those details (`RunView`) and renders a per-record `old → new` diff, with a summary
|
|
45
|
+
("42 of 120 records will change · 67 field updates").
|
|
46
|
+
3. **Confirm** — only on the user's click does it call `RunRecordProcess` again with `DryRun: false`. MJ's
|
|
47
|
+
built-in Record Changes versioning captures every before/after automatically.
|
|
48
|
+
|
|
49
|
+
Nothing is ever written until the user has seen precisely what will change.
|
|
50
|
+
|
|
51
|
+
## Wiring it into a grid (the full integration)
|
|
52
|
+
|
|
53
|
+
The MJ `entity-data-grid` already emits everything you need (`LoadEntityActionsRequested`,
|
|
54
|
+
`EntityActionRequested`) and exposes `ShowEntityActionButtons` to toggle visibility. The parent (an Explorer
|
|
55
|
+
view component) owns action resolution + mounting:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { EntityActionEngine } from '@memberjunction/actions';
|
|
59
|
+
import { EntityActionUXHostComponent, type EntityActionUXContext } from '@memberjunction/ng-entity-action-ux';
|
|
60
|
+
import type { EntityActionConfig } from '@memberjunction/ng-entity-viewer';
|
|
61
|
+
|
|
62
|
+
// 1) Resolve the entity's actions → grid configs (carry the driver key + its RecordProcessID param)
|
|
63
|
+
onLoadEntityActionsRequested(e: { entityInfo: EntityInfo }) {
|
|
64
|
+
const actions = EntityActionEngine.Instance.GetActionsByEntityName(e.entityInfo.Name, 'Active');
|
|
65
|
+
this.dataGrid.EntityActions = actions.map(a => ({
|
|
66
|
+
id: a.ID, name: a.Action, icon: 'fa-solid fa-list-check',
|
|
67
|
+
// the invocation's RuntimeUXDriverClass + the action's RecordProcessID param:
|
|
68
|
+
runtimeUXDriverClass: a.invocations.find(i => i.RuntimeUXDriverClass)?.RuntimeUXDriverClass,
|
|
69
|
+
metadata: { RecordProcessID: a.params.find(p => p.Name === 'RecordProcessID')?.Value },
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2) On request — mount the driver if one is named, else invoke normally
|
|
74
|
+
onEntityActionRequested(e: { entityInfo: EntityInfo; action: EntityActionConfig; selectedRecords: Record<string, unknown>[] }) {
|
|
75
|
+
if (e.action.runtimeUXDriverClass) {
|
|
76
|
+
this.activeDriver = {
|
|
77
|
+
DriverClass: e.action.runtimeUXDriverClass,
|
|
78
|
+
Context: <EntityActionUXContext>{
|
|
79
|
+
EntityInfo: e.entityInfo,
|
|
80
|
+
ScopeKind: 'records',
|
|
81
|
+
SelectedRecordIDs: e.selectedRecords.map(r => r[e.entityInfo.FirstPrimaryKey.Name] as string),
|
|
82
|
+
Config: { RecordProcessID: e.action.metadata?.['RecordProcessID'] },
|
|
83
|
+
Provider: this.Provider, // multi-provider: thread your provider through
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
/* existing: invoke the action directly */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<mj-entity-data-grid #dataGrid [Entity]="entityName" [ShowEntityActionButtons]="true"
|
|
94
|
+
(LoadEntityActionsRequested)="onLoadEntityActionsRequested($event)"
|
|
95
|
+
(EntityActionRequested)="onEntityActionRequested($event)">
|
|
96
|
+
</mj-entity-data-grid>
|
|
97
|
+
|
|
98
|
+
@if (activeDriver) {
|
|
99
|
+
<mj-entity-action-ux-host
|
|
100
|
+
[DriverClass]="activeDriver.DriverClass" [Context]="activeDriver.Context"
|
|
101
|
+
(Completed)="activeDriver = null; dataGrid.RefreshData()"
|
|
102
|
+
(Cancelled)="activeDriver = null">
|
|
103
|
+
</mj-entity-action-ux-host>
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> **Tree-shaking:** call `LoadEntityActionUX()` once from your app bootstrap so the driver's `@RegisterClass`
|
|
108
|
+
> side effect survives bundling (same pattern as other MJ dynamically-resolved classes).
|
|
109
|
+
|
|
110
|
+
## Server pieces (this feature ships them)
|
|
111
|
+
|
|
112
|
+
- **`RunRecordProcess` GraphQL mutation** (`@memberjunction/server`) → typed
|
|
113
|
+
**`GraphQLRecordProcessClient`** (`@memberjunction/graphql-dataprovider`). The runner uses the client; you
|
|
114
|
+
never inline `gql`.
|
|
115
|
+
- **`Run Record Process` Action** (`@memberjunction/core-actions`) — the agent / workflow / low-code surface
|
|
116
|
+
for the same engine. Seeded by [`metadata/actions/.record-processing.json`](../../../../metadata/actions/.record-processing.json).
|
|
117
|
+
- The engine is **`RecordProcessExecutor`** (`@memberjunction/record-set-processor`); FieldRules sit on the
|
|
118
|
+
pure rules engine in `@memberjunction/global`.
|
|
119
|
+
|
|
120
|
+
## Authoring rules (the builder)
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<mj-field-rules-builder [EntityName]="'Accounts'" [Provider]="Provider"
|
|
124
|
+
(ValueChange)="ruleSet = $event" (ValidChange)="canSave = $event">
|
|
125
|
+
</mj-field-rules-builder>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Serialize `ruleSet` into the Record Process's `Configuration` column (`JSON.stringify(ruleSet)`). The builder
|
|
129
|
+
validates live against the entity's metadata (`EntityFieldRules.Validate`) and shows per-rule errors.
|
|
130
|
+
|
|
131
|
+
## Demo metadata (copy-paste, adapt to your entity)
|
|
132
|
+
|
|
133
|
+
Wire the runner to an entity in three records. Seed a `MJ: Record Processes` (FieldRules), then an
|
|
134
|
+
`EntityAction` for the same entity whose invocation names the driver and whose param supplies the process ID:
|
|
135
|
+
|
|
136
|
+
```jsonc
|
|
137
|
+
// 1) the process — metadata/record-processes/.demo.json (entity: "MJ: Record Processes")
|
|
138
|
+
{ "fields": {
|
|
139
|
+
"Name": "Deactivate selected Accounts",
|
|
140
|
+
"EntityID": "@lookup:MJ: Entities.Name=Accounts",
|
|
141
|
+
"WorkType": "FieldRules",
|
|
142
|
+
"ScopeType": "Filter",
|
|
143
|
+
"Status": "Active",
|
|
144
|
+
"Configuration": "{\"Rules\":[{\"TargetField\":\"Status\",\"Source\":{\"Kind\":\"static\",\"Value\":\"Inactive\"}}]}"
|
|
145
|
+
} }
|
|
146
|
+
|
|
147
|
+
// 2) the entity action — links the generic "Run Record Process" action to the Accounts entity,
|
|
148
|
+
// names the driver on the invocation, and pins the RecordProcessID param.
|
|
149
|
+
{ "fields": {
|
|
150
|
+
"EntityID": "@lookup:MJ: Entities.Name=Accounts",
|
|
151
|
+
"ActionID": "@lookup:MJ: Actions.Name=Run Record Process",
|
|
152
|
+
"Status": "Active"
|
|
153
|
+
},
|
|
154
|
+
"relatedEntities": {
|
|
155
|
+
"MJ: Entity Action Invocations": [ { "fields": {
|
|
156
|
+
"InvocationTypeID": "@lookup:MJ: Entity Action Invocation Types.Name=Multi-Record",
|
|
157
|
+
"RuntimeUXDriverClass": "RecordProcessRunnerUX", "Status": "Active" } } ],
|
|
158
|
+
"MJ: Entity Action Params": [ { "fields": {
|
|
159
|
+
"Name": "RecordProcessID", "ValueType": "Static",
|
|
160
|
+
"Value": "@lookup:MJ: Record Processes.Name=Deactivate selected Accounts" } } ]
|
|
161
|
+
} }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Roadmap / proposed next work
|
|
165
|
+
|
|
166
|
+
- **Field-Rules authoring agent** — an AI agent that turns a natural-language instruction ("set Status to
|
|
167
|
+
Inactive for accounts with no activity in 12 months") into a validated `FieldRuleSet`, creates the Record
|
|
168
|
+
Process, and offers to run it via the `Run Record Process` action. Designed; intentionally not seeded here
|
|
169
|
+
because it needs a live system + prompt-tuning to validate.
|
|
170
|
+
- **`entityDocument` source** in the visual builder (engine already supports it; today it round-trips as a
|
|
171
|
+
static slot).
|
|
172
|
+
- **Saved-rule templates** and a side-by-side "before/after" record preview in the runner.
|
|
173
|
+
|
|
174
|
+
## Conventions
|
|
175
|
+
|
|
176
|
+
Standalone components · `inject()` DI · `@if`/`@for`/`@switch` · PascalCase public members · `--mj-*` design
|
|
177
|
+
tokens · confirm-left/cancel-right buttons · multi-provider via `Context.Provider` / `[Provider]`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A category-grouped, searchable AI-Prompt picker — the "slick selector" for choosing the
|
|
3
|
+
* prompt that powers a `prompt` field-rule source. Loads `MJ: AI Prompts`, groups them by category, and
|
|
4
|
+
* exposes the chosen prompt's ID via a two-way `Value`. Self-contained (no CDK overlay) so it drops into
|
|
5
|
+
* any form.
|
|
6
|
+
* @module @memberjunction/ng-entity-action-ux
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter, OnInit } from '@angular/core';
|
|
9
|
+
import { type IMetadataProvider } from '@memberjunction/core';
|
|
10
|
+
import { type PromptGroup, type PromptOption } from '../prompt-grouping';
|
|
11
|
+
import * as i0 from "@angular/core";
|
|
12
|
+
export declare class AIPromptSelectorComponent implements OnInit {
|
|
13
|
+
private cdr;
|
|
14
|
+
private host;
|
|
15
|
+
/** The selected prompt's ID (two-way bindable). */
|
|
16
|
+
Value: string | null;
|
|
17
|
+
ValueChange: EventEmitter<string | null>;
|
|
18
|
+
/** Placeholder shown when nothing is selected. */
|
|
19
|
+
Placeholder: string;
|
|
20
|
+
/** Metadata provider (multi-provider). Falls back to the global default when null. */
|
|
21
|
+
Provider: IMetadataProvider | null;
|
|
22
|
+
IsOpen: boolean;
|
|
23
|
+
Loading: boolean;
|
|
24
|
+
Filter: string;
|
|
25
|
+
Groups: PromptGroup[];
|
|
26
|
+
private allPrompts;
|
|
27
|
+
ngOnInit(): Promise<void>;
|
|
28
|
+
get SelectedLabel(): string;
|
|
29
|
+
/** True when the given prompt is the selected one (UUID-safe across SQL Server / PostgreSQL). */
|
|
30
|
+
IsSelected(prompt: PromptOption): boolean;
|
|
31
|
+
get FilteredGroups(): PromptGroup[];
|
|
32
|
+
Toggle(): void;
|
|
33
|
+
Select(p: PromptOption): void;
|
|
34
|
+
OnFilter(event: Event): void;
|
|
35
|
+
/** Close when clicking outside the component. */
|
|
36
|
+
onDocumentClick(event: MouseEvent): void;
|
|
37
|
+
private loadPrompts;
|
|
38
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<AIPromptSelectorComponent, never>;
|
|
39
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<AIPromptSelectorComponent, "mj-ai-prompt-selector", never, { "Value": { "alias": "Value"; "required": false; }; "Placeholder": { "alias": "Placeholder"; "required": false; }; "Provider": { "alias": "Provider"; "required": false; }; }, { "ValueChange": "ValueChange"; }, never, never, true, never>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=ai-prompt-selector.component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-prompt-selector.component.d.ts","sourceRoot":"","sources":["../../../src/lib/ai-prompt-selector/ai-prompt-selector.component.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAKH,YAAY,EAGZ,MAAM,EAGT,MAAM,eAAe,CAAC;AACvB,OAAO,EAAW,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEvE,OAAO,EAA8C,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;;AAErH,qBAyEa,yBAA0B,YAAW,MAAM;IACpD,OAAO,CAAC,GAAG,CAA6B;IACxC,OAAO,CAAC,IAAI,CAAsB;IAElC,mDAAmD;IAC1C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC3B,WAAW,8BAAqC;IAE1D,kDAAkD;IACzC,WAAW,SAA0B;IAE9C,sFAAsF;IAC7E,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAQ;IAE5C,MAAM,UAAS;IACf,OAAO,UAAQ;IACf,MAAM,SAAM;IACZ,MAAM,EAAE,WAAW,EAAE,CAAM;IAElC,OAAO,CAAC,UAAU,CAAsB;IAElC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,iGAAiG;IACjG,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO;IAIzC,IAAI,cAAc,IAAI,WAAW,EAAE,CAElC;IAED,MAAM,IAAI,IAAI;IAMd,MAAM,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI;IAO7B,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAK5B,iDAAiD;IAEjD,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;YAO1B,WAAW;yCAjEhB,yBAAyB;2CAAzB,yBAAyB;CA+ErC"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A category-grouped, searchable AI-Prompt picker — the "slick selector" for choosing the
|
|
3
|
+
* prompt that powers a `prompt` field-rule source. Loads `MJ: AI Prompts`, groups them by category, and
|
|
4
|
+
* exposes the chosen prompt's ID via a two-way `Value`. Self-contained (no CDK overlay) so it drops into
|
|
5
|
+
* any form.
|
|
6
|
+
* @module @memberjunction/ng-entity-action-ux
|
|
7
|
+
*/
|
|
8
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, inject, } from '@angular/core';
|
|
9
|
+
import { RunView } from '@memberjunction/core';
|
|
10
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
11
|
+
import { filterPromptGroups, groupPromptsByCategory } from '../prompt-grouping';
|
|
12
|
+
import * as i0 from "@angular/core";
|
|
13
|
+
const _forTrack0 = ($index, $item) => $item.Category;
|
|
14
|
+
const _forTrack1 = ($index, $item) => $item.ID;
|
|
15
|
+
function AIPromptSelectorComponent_Conditional_6_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
16
|
+
i0.ɵɵdomElementStart(0, "div", 11);
|
|
17
|
+
i0.ɵɵtext(1, "Loading prompts\u2026");
|
|
18
|
+
i0.ɵɵdomElementEnd();
|
|
19
|
+
} }
|
|
20
|
+
function AIPromptSelectorComponent_Conditional_6_Conditional_7_Template(rf, ctx) { if (rf & 1) {
|
|
21
|
+
i0.ɵɵdomElementStart(0, "div", 11);
|
|
22
|
+
i0.ɵɵtext(1);
|
|
23
|
+
i0.ɵɵdomElementEnd();
|
|
24
|
+
} if (rf & 2) {
|
|
25
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
26
|
+
i0.ɵɵadvance();
|
|
27
|
+
i0.ɵɵtextInterpolate1("No prompts match \u201C", ctx_r1.Filter, "\u201D.");
|
|
28
|
+
} }
|
|
29
|
+
function AIPromptSelectorComponent_Conditional_6_Conditional_8_For_1_For_3_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
30
|
+
i0.ɵɵdomElement(0, "i", 15);
|
|
31
|
+
} }
|
|
32
|
+
function AIPromptSelectorComponent_Conditional_6_Conditional_8_For_1_For_3_Template(rf, ctx) { if (rf & 1) {
|
|
33
|
+
const _r3 = i0.ɵɵgetCurrentView();
|
|
34
|
+
i0.ɵɵdomElementStart(0, "div", 14);
|
|
35
|
+
i0.ɵɵdomListener("click", function AIPromptSelectorComponent_Conditional_6_Conditional_8_For_1_For_3_Template_div_click_0_listener() { const p_r4 = i0.ɵɵrestoreView(_r3).$implicit; const ctx_r1 = i0.ɵɵnextContext(4); return i0.ɵɵresetView(ctx_r1.Select(p_r4)); });
|
|
36
|
+
i0.ɵɵdomElementStart(1, "span");
|
|
37
|
+
i0.ɵɵtext(2);
|
|
38
|
+
i0.ɵɵdomElementEnd();
|
|
39
|
+
i0.ɵɵconditionalCreate(3, AIPromptSelectorComponent_Conditional_6_Conditional_8_For_1_For_3_Conditional_3_Template, 1, 0, "i", 15);
|
|
40
|
+
i0.ɵɵdomElementEnd();
|
|
41
|
+
} if (rf & 2) {
|
|
42
|
+
const p_r4 = ctx.$implicit;
|
|
43
|
+
const ctx_r1 = i0.ɵɵnextContext(4);
|
|
44
|
+
i0.ɵɵclassProp("ps-option--selected", ctx_r1.IsSelected(p_r4));
|
|
45
|
+
i0.ɵɵadvance(2);
|
|
46
|
+
i0.ɵɵtextInterpolate(p_r4.Name);
|
|
47
|
+
i0.ɵɵadvance();
|
|
48
|
+
i0.ɵɵconditional(ctx_r1.IsSelected(p_r4) ? 3 : -1);
|
|
49
|
+
} }
|
|
50
|
+
function AIPromptSelectorComponent_Conditional_6_Conditional_8_For_1_Template(rf, ctx) { if (rf & 1) {
|
|
51
|
+
i0.ɵɵdomElementStart(0, "div", 12);
|
|
52
|
+
i0.ɵɵtext(1);
|
|
53
|
+
i0.ɵɵdomElementEnd();
|
|
54
|
+
i0.ɵɵrepeaterCreate(2, AIPromptSelectorComponent_Conditional_6_Conditional_8_For_1_For_3_Template, 4, 4, "div", 13, _forTrack1);
|
|
55
|
+
} if (rf & 2) {
|
|
56
|
+
const group_r5 = ctx.$implicit;
|
|
57
|
+
i0.ɵɵadvance();
|
|
58
|
+
i0.ɵɵtextInterpolate(group_r5.Category);
|
|
59
|
+
i0.ɵɵadvance();
|
|
60
|
+
i0.ɵɵrepeater(group_r5.Prompts);
|
|
61
|
+
} }
|
|
62
|
+
function AIPromptSelectorComponent_Conditional_6_Conditional_8_Template(rf, ctx) { if (rf & 1) {
|
|
63
|
+
i0.ɵɵrepeaterCreate(0, AIPromptSelectorComponent_Conditional_6_Conditional_8_For_1_Template, 4, 1, null, null, _forTrack0);
|
|
64
|
+
} if (rf & 2) {
|
|
65
|
+
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
66
|
+
i0.ɵɵrepeater(ctx_r1.FilteredGroups);
|
|
67
|
+
} }
|
|
68
|
+
function AIPromptSelectorComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
69
|
+
const _r1 = i0.ɵɵgetCurrentView();
|
|
70
|
+
i0.ɵɵdomElementStart(0, "div", 6)(1, "div", 7);
|
|
71
|
+
i0.ɵɵdomElement(2, "i", 8);
|
|
72
|
+
i0.ɵɵdomElementStart(3, "input", 9, 0);
|
|
73
|
+
i0.ɵɵdomListener("input", function AIPromptSelectorComponent_Conditional_6_Template_input_input_3_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.OnFilter($event)); });
|
|
74
|
+
i0.ɵɵdomElementEnd()();
|
|
75
|
+
i0.ɵɵdomElementStart(5, "div", 10);
|
|
76
|
+
i0.ɵɵconditionalCreate(6, AIPromptSelectorComponent_Conditional_6_Conditional_6_Template, 2, 0, "div", 11)(7, AIPromptSelectorComponent_Conditional_6_Conditional_7_Template, 2, 1, "div", 11)(8, AIPromptSelectorComponent_Conditional_6_Conditional_8_Template, 2, 0);
|
|
77
|
+
i0.ɵɵdomElementEnd()();
|
|
78
|
+
} if (rf & 2) {
|
|
79
|
+
const ctx_r1 = i0.ɵɵnextContext();
|
|
80
|
+
i0.ɵɵadvance(3);
|
|
81
|
+
i0.ɵɵdomProperty("value", ctx_r1.Filter);
|
|
82
|
+
i0.ɵɵadvance(3);
|
|
83
|
+
i0.ɵɵconditional(ctx_r1.Loading ? 6 : ctx_r1.FilteredGroups.length === 0 ? 7 : 8);
|
|
84
|
+
} }
|
|
85
|
+
export class AIPromptSelectorComponent {
|
|
86
|
+
cdr = inject(ChangeDetectorRef);
|
|
87
|
+
host = inject(ElementRef);
|
|
88
|
+
/** The selected prompt's ID (two-way bindable). */
|
|
89
|
+
Value = null;
|
|
90
|
+
ValueChange = new EventEmitter();
|
|
91
|
+
/** Placeholder shown when nothing is selected. */
|
|
92
|
+
Placeholder = 'Select an AI prompt…';
|
|
93
|
+
/** Metadata provider (multi-provider). Falls back to the global default when null. */
|
|
94
|
+
Provider = null;
|
|
95
|
+
IsOpen = false;
|
|
96
|
+
Loading = true;
|
|
97
|
+
Filter = '';
|
|
98
|
+
Groups = [];
|
|
99
|
+
allPrompts = [];
|
|
100
|
+
async ngOnInit() {
|
|
101
|
+
await this.loadPrompts();
|
|
102
|
+
}
|
|
103
|
+
get SelectedLabel() {
|
|
104
|
+
return this.allPrompts.find((p) => UUIDsEqual(p.ID, this.Value ?? ''))?.Name ?? '';
|
|
105
|
+
}
|
|
106
|
+
/** True when the given prompt is the selected one (UUID-safe across SQL Server / PostgreSQL). */
|
|
107
|
+
IsSelected(prompt) {
|
|
108
|
+
return UUIDsEqual(prompt.ID, this.Value ?? '');
|
|
109
|
+
}
|
|
110
|
+
get FilteredGroups() {
|
|
111
|
+
return filterPromptGroups(this.Groups, this.Filter);
|
|
112
|
+
}
|
|
113
|
+
Toggle() {
|
|
114
|
+
this.IsOpen = !this.IsOpen;
|
|
115
|
+
if (this.IsOpen)
|
|
116
|
+
this.Filter = '';
|
|
117
|
+
this.cdr.detectChanges();
|
|
118
|
+
}
|
|
119
|
+
Select(p) {
|
|
120
|
+
this.Value = p.ID;
|
|
121
|
+
this.ValueChange.emit(p.ID);
|
|
122
|
+
this.IsOpen = false;
|
|
123
|
+
this.cdr.detectChanges();
|
|
124
|
+
}
|
|
125
|
+
OnFilter(event) {
|
|
126
|
+
this.Filter = event.target.value;
|
|
127
|
+
this.cdr.detectChanges();
|
|
128
|
+
}
|
|
129
|
+
/** Close when clicking outside the component. */
|
|
130
|
+
onDocumentClick(event) {
|
|
131
|
+
if (this.IsOpen && !this.host.nativeElement.contains(event.target)) {
|
|
132
|
+
this.IsOpen = false;
|
|
133
|
+
this.cdr.detectChanges();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async loadPrompts() {
|
|
137
|
+
const runView = this.Provider ? RunView.FromMetadataProvider(this.Provider) : new RunView();
|
|
138
|
+
const rv = await runView.RunView({
|
|
139
|
+
EntityName: 'MJ: AI Prompts',
|
|
140
|
+
Fields: ['ID', 'Name', 'Category'],
|
|
141
|
+
ExtraFilter: `Status='Active'`,
|
|
142
|
+
OrderBy: 'Category, Name',
|
|
143
|
+
ResultType: 'simple',
|
|
144
|
+
});
|
|
145
|
+
this.allPrompts = rv.Success ? (rv.Results ?? []) : [];
|
|
146
|
+
this.Groups = groupPromptsByCategory(this.allPrompts);
|
|
147
|
+
this.Loading = false;
|
|
148
|
+
this.cdr.detectChanges();
|
|
149
|
+
}
|
|
150
|
+
static ɵfac = function AIPromptSelectorComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || AIPromptSelectorComponent)(); };
|
|
151
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AIPromptSelectorComponent, selectors: [["mj-ai-prompt-selector"]], hostBindings: function AIPromptSelectorComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
152
|
+
i0.ɵɵlistener("click", function AIPromptSelectorComponent_click_HostBindingHandler($event) { return ctx.onDocumentClick($event); }, i0.ɵɵresolveDocument);
|
|
153
|
+
} }, inputs: { Value: "Value", Placeholder: "Placeholder", Provider: "Provider" }, outputs: { ValueChange: "ValueChange" }, decls: 7, vars: 7, consts: [["search", ""], [1, "ps"], ["type", "button", 1, "ps-trigger", 3, "click"], [1, "ps-trigger-text"], [1, "fa-solid", "fa-wand-magic-sparkles"], [1, "fa-solid", "fa-chevron-down", "ps-caret"], ["role", "listbox", 1, "ps-panel"], [1, "ps-search"], [1, "fa-solid", "fa-magnifying-glass"], ["type", "text", "placeholder", "Search prompts\u2026", 1, "mj-input", 3, "input", "value"], [1, "ps-list"], [1, "ps-empty"], [1, "ps-group-label"], ["role", "option", 1, "ps-option", 3, "ps-option--selected"], ["role", "option", 1, "ps-option", 3, "click"], [1, "fa-solid", "fa-check"]], template: function AIPromptSelectorComponent_Template(rf, ctx) { if (rf & 1) {
|
|
154
|
+
i0.ɵɵdomElementStart(0, "div", 1)(1, "button", 2);
|
|
155
|
+
i0.ɵɵdomListener("click", function AIPromptSelectorComponent_Template_button_click_1_listener() { return ctx.Toggle(); });
|
|
156
|
+
i0.ɵɵdomElementStart(2, "span", 3);
|
|
157
|
+
i0.ɵɵdomElement(3, "i", 4);
|
|
158
|
+
i0.ɵɵtext(4);
|
|
159
|
+
i0.ɵɵdomElementEnd();
|
|
160
|
+
i0.ɵɵdomElement(5, "i", 5);
|
|
161
|
+
i0.ɵɵdomElementEnd();
|
|
162
|
+
i0.ɵɵconditionalCreate(6, AIPromptSelectorComponent_Conditional_6_Template, 9, 2, "div", 6);
|
|
163
|
+
i0.ɵɵdomElementEnd();
|
|
164
|
+
} if (rf & 2) {
|
|
165
|
+
i0.ɵɵclassProp("ps--open", ctx.IsOpen);
|
|
166
|
+
i0.ɵɵadvance();
|
|
167
|
+
i0.ɵɵattribute("aria-expanded", ctx.IsOpen);
|
|
168
|
+
i0.ɵɵadvance();
|
|
169
|
+
i0.ɵɵclassProp("ps-placeholder", !ctx.SelectedLabel);
|
|
170
|
+
i0.ɵɵadvance(2);
|
|
171
|
+
i0.ɵɵtextInterpolate1(" ", ctx.SelectedLabel || ctx.Placeholder, " ");
|
|
172
|
+
i0.ɵɵadvance(2);
|
|
173
|
+
i0.ɵɵconditional(ctx.IsOpen ? 6 : -1);
|
|
174
|
+
} }, styles: [".ps[_ngcontent-%COMP%] { position: relative; width: 100%; }\n .ps-trigger[_ngcontent-%COMP%] { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px;\n padding: 8px 12px; background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px); color: var(--mj-text-primary); cursor: pointer; font-size: 14px; }\n .ps--open[_ngcontent-%COMP%] .ps-trigger[_ngcontent-%COMP%] { border-color: var(--mj-border-focus); }\n .ps-trigger-text[_ngcontent-%COMP%] { display: flex; align-items: center; gap: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ps-trigger-text[_ngcontent-%COMP%] i[_ngcontent-%COMP%] { color: var(--mj-brand-primary); }\n .ps-placeholder[_ngcontent-%COMP%] { color: var(--mj-text-muted); }\n .ps-caret[_ngcontent-%COMP%] { color: var(--mj-text-muted); font-size: 12px; transition: transform .15s ease; }\n .ps--open[_ngcontent-%COMP%] .ps-caret[_ngcontent-%COMP%] { transform: rotate(180deg); }\n .ps-panel[_ngcontent-%COMP%] { position: absolute; z-index: 50; top: calc(100% + 4px); left: 0; right: 0; max-height: 320px;\n display: flex; flex-direction: column; background: var(--mj-bg-surface-elevated, var(--mj-bg-surface));\n border: 1px solid var(--mj-border-default); border-radius: var(--mj-radius-md, 8px);\n box-shadow: var(--mj-shadow-md, 0 6px 20px rgba(0,0,0,.12)); overflow: hidden; }\n .ps-search[_ngcontent-%COMP%] { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--mj-border-subtle); }\n .ps-search[_ngcontent-%COMP%] i[_ngcontent-%COMP%] { color: var(--mj-text-muted); }\n .ps-search[_ngcontent-%COMP%] input[_ngcontent-%COMP%] { flex: 1; border: none; outline: none; background: transparent; color: var(--mj-text-primary); }\n .ps-list[_ngcontent-%COMP%] { overflow-y: auto; padding: 4px 0; }\n .ps-group-label[_ngcontent-%COMP%] { padding: 6px 12px 2px; font-size: 11px; font-weight: 700; text-transform: uppercase;\n letter-spacing: .04em; color: var(--mj-text-muted); }\n .ps-option[_ngcontent-%COMP%] { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 7px 12px 7px 22px;\n cursor: pointer; font-size: 14px; color: var(--mj-text-primary); }\n .ps-option[_ngcontent-%COMP%]:hover { background: var(--mj-bg-surface-hover); }\n .ps-option--selected[_ngcontent-%COMP%] { color: var(--mj-brand-primary); font-weight: 600; }\n .ps-option[_ngcontent-%COMP%] i[_ngcontent-%COMP%] { color: var(--mj-brand-primary); }\n .ps-empty[_ngcontent-%COMP%] { padding: 16px; text-align: center; color: var(--mj-text-muted); font-size: 13px; }"], changeDetection: 0 });
|
|
175
|
+
}
|
|
176
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AIPromptSelectorComponent, [{
|
|
177
|
+
type: Component,
|
|
178
|
+
args: [{ selector: 'mj-ai-prompt-selector', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
179
|
+
<div class="ps" [class.ps--open]="IsOpen">
|
|
180
|
+
<button type="button" class="ps-trigger" (click)="Toggle()" [attr.aria-expanded]="IsOpen">
|
|
181
|
+
<span class="ps-trigger-text" [class.ps-placeholder]="!SelectedLabel">
|
|
182
|
+
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
|
183
|
+
{{ SelectedLabel || Placeholder }}
|
|
184
|
+
</span>
|
|
185
|
+
<i class="fa-solid fa-chevron-down ps-caret"></i>
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
@if (IsOpen) {
|
|
189
|
+
<div class="ps-panel" role="listbox">
|
|
190
|
+
<div class="ps-search">
|
|
191
|
+
<i class="fa-solid fa-magnifying-glass"></i>
|
|
192
|
+
<input #search type="text" class="mj-input" placeholder="Search prompts…"
|
|
193
|
+
[value]="Filter" (input)="OnFilter($event)" />
|
|
194
|
+
</div>
|
|
195
|
+
<div class="ps-list">
|
|
196
|
+
@if (Loading) {
|
|
197
|
+
<div class="ps-empty">Loading prompts…</div>
|
|
198
|
+
} @else if (FilteredGroups.length === 0) {
|
|
199
|
+
<div class="ps-empty">No prompts match “{{ Filter }}”.</div>
|
|
200
|
+
} @else {
|
|
201
|
+
@for (group of FilteredGroups; track group.Category) {
|
|
202
|
+
<div class="ps-group-label">{{ group.Category }}</div>
|
|
203
|
+
@for (p of group.Prompts; track p.ID) {
|
|
204
|
+
<div class="ps-option" role="option"
|
|
205
|
+
[class.ps-option--selected]="IsSelected(p)"
|
|
206
|
+
(click)="Select(p)">
|
|
207
|
+
<span>{{ p.Name }}</span>
|
|
208
|
+
@if (IsSelected(p)) { <i class="fa-solid fa-check"></i> }
|
|
209
|
+
</div>
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
}
|
|
216
|
+
</div>
|
|
217
|
+
`, styles: ["\n .ps { position: relative; width: 100%; }\n .ps-trigger { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px;\n padding: 8px 12px; background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px); color: var(--mj-text-primary); cursor: pointer; font-size: 14px; }\n .ps--open .ps-trigger { border-color: var(--mj-border-focus); }\n .ps-trigger-text { display: flex; align-items: center; gap: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ps-trigger-text i { color: var(--mj-brand-primary); }\n .ps-placeholder { color: var(--mj-text-muted); }\n .ps-caret { color: var(--mj-text-muted); font-size: 12px; transition: transform .15s ease; }\n .ps--open .ps-caret { transform: rotate(180deg); }\n .ps-panel { position: absolute; z-index: 50; top: calc(100% + 4px); left: 0; right: 0; max-height: 320px;\n display: flex; flex-direction: column; background: var(--mj-bg-surface-elevated, var(--mj-bg-surface));\n border: 1px solid var(--mj-border-default); border-radius: var(--mj-radius-md, 8px);\n box-shadow: var(--mj-shadow-md, 0 6px 20px rgba(0,0,0,.12)); overflow: hidden; }\n .ps-search { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--mj-border-subtle); }\n .ps-search i { color: var(--mj-text-muted); }\n .ps-search input { flex: 1; border: none; outline: none; background: transparent; color: var(--mj-text-primary); }\n .ps-list { overflow-y: auto; padding: 4px 0; }\n .ps-group-label { padding: 6px 12px 2px; font-size: 11px; font-weight: 700; text-transform: uppercase;\n letter-spacing: .04em; color: var(--mj-text-muted); }\n .ps-option { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 7px 12px 7px 22px;\n cursor: pointer; font-size: 14px; color: var(--mj-text-primary); }\n .ps-option:hover { background: var(--mj-bg-surface-hover); }\n .ps-option--selected { color: var(--mj-brand-primary); font-weight: 600; }\n .ps-option i { color: var(--mj-brand-primary); }\n .ps-empty { padding: 16px; text-align: center; color: var(--mj-text-muted); font-size: 13px; }\n "] }]
|
|
218
|
+
}], null, { Value: [{
|
|
219
|
+
type: Input
|
|
220
|
+
}], ValueChange: [{
|
|
221
|
+
type: Output
|
|
222
|
+
}], Placeholder: [{
|
|
223
|
+
type: Input
|
|
224
|
+
}], Provider: [{
|
|
225
|
+
type: Input
|
|
226
|
+
}], onDocumentClick: [{
|
|
227
|
+
type: HostListener,
|
|
228
|
+
args: ['document:click', ['$event']]
|
|
229
|
+
}] }); })();
|
|
230
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AIPromptSelectorComponent, { className: "AIPromptSelectorComponent", filePath: "lib/ai-prompt-selector/ai-prompt-selector.component.ts", lineNumber: 97 }); })();
|
|
231
|
+
//# sourceMappingURL=ai-prompt-selector.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-prompt-selector.component.js","sourceRoot":"","sources":["../../../src/lib/ai-prompt-selector/ai-prompt-selector.component.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview A category-grouped, searchable AI-Prompt picker — the \"slick selector\" for choosing the\n * prompt that powers a `prompt` field-rule source. Loads `MJ: AI Prompts`, groups them by category, and\n * exposes the chosen prompt's ID via a two-way `Value`. Self-contained (no CDK overlay) so it drops into\n * any form.\n * @module @memberjunction/ng-entity-action-ux\n */\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ElementRef,\n EventEmitter,\n HostListener,\n Input,\n OnInit,\n Output,\n inject,\n} from '@angular/core';\nimport { RunView, type IMetadataProvider } from '@memberjunction/core';\nimport { UUIDsEqual } from '@memberjunction/global';\nimport { filterPromptGroups, groupPromptsByCategory, type PromptGroup, type PromptOption } from '../prompt-grouping';\n\n@Component({\n selector: 'mj-ai-prompt-selector',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div class=\"ps\" [class.ps--open]=\"IsOpen\">\n <button type=\"button\" class=\"ps-trigger\" (click)=\"Toggle()\" [attr.aria-expanded]=\"IsOpen\">\n <span class=\"ps-trigger-text\" [class.ps-placeholder]=\"!SelectedLabel\">\n <i class=\"fa-solid fa-wand-magic-sparkles\"></i>\n {{ SelectedLabel || Placeholder }}\n </span>\n <i class=\"fa-solid fa-chevron-down ps-caret\"></i>\n </button>\n\n @if (IsOpen) {\n <div class=\"ps-panel\" role=\"listbox\">\n <div class=\"ps-search\">\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n <input #search type=\"text\" class=\"mj-input\" placeholder=\"Search prompts…\"\n [value]=\"Filter\" (input)=\"OnFilter($event)\" />\n </div>\n <div class=\"ps-list\">\n @if (Loading) {\n <div class=\"ps-empty\">Loading prompts…</div>\n } @else if (FilteredGroups.length === 0) {\n <div class=\"ps-empty\">No prompts match “{{ Filter }}”.</div>\n } @else {\n @for (group of FilteredGroups; track group.Category) {\n <div class=\"ps-group-label\">{{ group.Category }}</div>\n @for (p of group.Prompts; track p.ID) {\n <div class=\"ps-option\" role=\"option\"\n [class.ps-option--selected]=\"IsSelected(p)\"\n (click)=\"Select(p)\">\n <span>{{ p.Name }}</span>\n @if (IsSelected(p)) { <i class=\"fa-solid fa-check\"></i> }\n </div>\n }\n }\n }\n </div>\n </div>\n }\n </div>\n `,\n styles: [`\n .ps { position: relative; width: 100%; }\n .ps-trigger { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px;\n padding: 8px 12px; background: var(--mj-bg-surface); border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px); color: var(--mj-text-primary); cursor: pointer; font-size: 14px; }\n .ps--open .ps-trigger { border-color: var(--mj-border-focus); }\n .ps-trigger-text { display: flex; align-items: center; gap: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ps-trigger-text i { color: var(--mj-brand-primary); }\n .ps-placeholder { color: var(--mj-text-muted); }\n .ps-caret { color: var(--mj-text-muted); font-size: 12px; transition: transform .15s ease; }\n .ps--open .ps-caret { transform: rotate(180deg); }\n .ps-panel { position: absolute; z-index: 50; top: calc(100% + 4px); left: 0; right: 0; max-height: 320px;\n display: flex; flex-direction: column; background: var(--mj-bg-surface-elevated, var(--mj-bg-surface));\n border: 1px solid var(--mj-border-default); border-radius: var(--mj-radius-md, 8px);\n box-shadow: var(--mj-shadow-md, 0 6px 20px rgba(0,0,0,.12)); overflow: hidden; }\n .ps-search { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--mj-border-subtle); }\n .ps-search i { color: var(--mj-text-muted); }\n .ps-search input { flex: 1; border: none; outline: none; background: transparent; color: var(--mj-text-primary); }\n .ps-list { overflow-y: auto; padding: 4px 0; }\n .ps-group-label { padding: 6px 12px 2px; font-size: 11px; font-weight: 700; text-transform: uppercase;\n letter-spacing: .04em; color: var(--mj-text-muted); }\n .ps-option { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 7px 12px 7px 22px;\n cursor: pointer; font-size: 14px; color: var(--mj-text-primary); }\n .ps-option:hover { background: var(--mj-bg-surface-hover); }\n .ps-option--selected { color: var(--mj-brand-primary); font-weight: 600; }\n .ps-option i { color: var(--mj-brand-primary); }\n .ps-empty { padding: 16px; text-align: center; color: var(--mj-text-muted); font-size: 13px; }\n `],\n})\nexport class AIPromptSelectorComponent implements OnInit {\n private cdr = inject(ChangeDetectorRef);\n private host = inject(ElementRef);\n\n /** The selected prompt's ID (two-way bindable). */\n @Input() Value: string | null = null;\n @Output() ValueChange = new EventEmitter<string | null>();\n\n /** Placeholder shown when nothing is selected. */\n @Input() Placeholder = 'Select an AI prompt…';\n\n /** Metadata provider (multi-provider). Falls back to the global default when null. */\n @Input() Provider: IMetadataProvider | null = null;\n\n public IsOpen = false;\n public Loading = true;\n public Filter = '';\n public Groups: PromptGroup[] = [];\n\n private allPrompts: PromptOption[] = [];\n\n async ngOnInit(): Promise<void> {\n await this.loadPrompts();\n }\n\n get SelectedLabel(): string {\n return this.allPrompts.find((p) => UUIDsEqual(p.ID, this.Value ?? ''))?.Name ?? '';\n }\n\n /** True when the given prompt is the selected one (UUID-safe across SQL Server / PostgreSQL). */\n IsSelected(prompt: PromptOption): boolean {\n return UUIDsEqual(prompt.ID, this.Value ?? '');\n }\n\n get FilteredGroups(): PromptGroup[] {\n return filterPromptGroups(this.Groups, this.Filter);\n }\n\n Toggle(): void {\n this.IsOpen = !this.IsOpen;\n if (this.IsOpen) this.Filter = '';\n this.cdr.detectChanges();\n }\n\n Select(p: PromptOption): void {\n this.Value = p.ID;\n this.ValueChange.emit(p.ID);\n this.IsOpen = false;\n this.cdr.detectChanges();\n }\n\n OnFilter(event: Event): void {\n this.Filter = (event.target as HTMLInputElement).value;\n this.cdr.detectChanges();\n }\n\n /** Close when clicking outside the component. */\n @HostListener('document:click', ['$event'])\n onDocumentClick(event: MouseEvent): void {\n if (this.IsOpen && !this.host.nativeElement.contains(event.target)) {\n this.IsOpen = false;\n this.cdr.detectChanges();\n }\n }\n\n private async loadPrompts(): Promise<void> {\n const runView = this.Provider ? RunView.FromMetadataProvider(this.Provider) : new RunView();\n const rv = await runView.RunView<PromptOption>({\n EntityName: 'MJ: AI Prompts',\n Fields: ['ID', 'Name', 'Category'],\n ExtraFilter: `Status='Active'`,\n OrderBy: 'Category, Name',\n ResultType: 'simple',\n });\n this.allPrompts = rv.Success ? (rv.Results ?? []) : [];\n this.Groups = groupPromptsByCategory(this.allPrompts);\n this.Loading = false;\n this.cdr.detectChanges();\n }\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The plugin contract for an entity-action runtime UX. A concrete driver is a standalone
|
|
3
|
+
* Angular component that extends this base and registers itself with `@RegisterClass(BaseEntityActionRuntimeUX,
|
|
4
|
+
* '<DriverKey>')`. The host ({@link EntityActionUXHostComponent}) resolves the key via the MJ ClassFactory,
|
|
5
|
+
* mounts the component, hands it a {@link EntityActionUXContext}, calls {@link Start}, and listens for the
|
|
6
|
+
* `Completed` / `Cancelled` outputs. New drivers ship in this package or any consumer — no host changes.
|
|
7
|
+
* @module @memberjunction/ng-entity-action-ux
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from '@angular/core';
|
|
10
|
+
import type { EntityActionUXContext, EntityActionUXResult } from './runtime-ux-context';
|
|
11
|
+
import * as i0 from "@angular/core";
|
|
12
|
+
/**
|
|
13
|
+
* Abstract base for a runtime-UX driver. `@Directive()` (not `@Component`) so it can be a decorated,
|
|
14
|
+
* DI-capable base class without its own template; concrete drivers supply the `@Component`.
|
|
15
|
+
*/
|
|
16
|
+
export declare abstract class BaseEntityActionRuntimeUX {
|
|
17
|
+
/** The work context, set by the host immediately after construction (before {@link Start}). */
|
|
18
|
+
Context: EntityActionUXContext;
|
|
19
|
+
/** Emit when the driver has finished its work (applied or not — see {@link EntityActionUXResult}). */
|
|
20
|
+
Completed: EventEmitter<EntityActionUXResult>;
|
|
21
|
+
/** Emit when the user dismisses the driver without completing. */
|
|
22
|
+
Cancelled: EventEmitter<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Begin the driver's flow. Called once by the host after `Context` is set. Implementations typically
|
|
25
|
+
* kick off an async preview/load here and drive their own template state from it.
|
|
26
|
+
*/
|
|
27
|
+
abstract Start(): void | Promise<void>;
|
|
28
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<BaseEntityActionRuntimeUX, never>;
|
|
29
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<BaseEntityActionRuntimeUX, never, never, { "Context": { "alias": "Context"; "required": false; }; }, { "Completed": "Completed"; "Cancelled": "Cancelled"; }, never, never, true, never>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=base-entity-action-runtime-ux.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-entity-action-runtime-ux.d.ts","sourceRoot":"","sources":["../../src/lib/base-entity-action-runtime-ux.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAa,YAAY,EAAiB,MAAM,eAAe,CAAC;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;;AAExF;;;GAGG;AACH,8BACsB,yBAAyB;IAC3C,+FAA+F;IACtF,OAAO,EAAG,qBAAqB,CAAC;IAEzC,sGAAsG;IAC5F,SAAS,qCAA4C;IAE/D,kEAAkE;IACxD,SAAS,qBAA4B;IAE/C;;;OAGG;IACH,QAAQ,CAAC,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;yCAdpB,yBAAyB;2CAAzB,yBAAyB;CAe9C"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The plugin contract for an entity-action runtime UX. A concrete driver is a standalone
|
|
3
|
+
* Angular component that extends this base and registers itself with `@RegisterClass(BaseEntityActionRuntimeUX,
|
|
4
|
+
* '<DriverKey>')`. The host ({@link EntityActionUXHostComponent}) resolves the key via the MJ ClassFactory,
|
|
5
|
+
* mounts the component, hands it a {@link EntityActionUXContext}, calls {@link Start}, and listens for the
|
|
6
|
+
* `Completed` / `Cancelled` outputs. New drivers ship in this package or any consumer — no host changes.
|
|
7
|
+
* @module @memberjunction/ng-entity-action-ux
|
|
8
|
+
*/
|
|
9
|
+
import { Directive, EventEmitter, Input, Output } from '@angular/core';
|
|
10
|
+
import * as i0 from "@angular/core";
|
|
11
|
+
/**
|
|
12
|
+
* Abstract base for a runtime-UX driver. `@Directive()` (not `@Component`) so it can be a decorated,
|
|
13
|
+
* DI-capable base class without its own template; concrete drivers supply the `@Component`.
|
|
14
|
+
*/
|
|
15
|
+
export class BaseEntityActionRuntimeUX {
|
|
16
|
+
/** The work context, set by the host immediately after construction (before {@link Start}). */
|
|
17
|
+
Context;
|
|
18
|
+
/** Emit when the driver has finished its work (applied or not — see {@link EntityActionUXResult}). */
|
|
19
|
+
Completed = new EventEmitter();
|
|
20
|
+
/** Emit when the user dismisses the driver without completing. */
|
|
21
|
+
Cancelled = new EventEmitter();
|
|
22
|
+
static ɵfac = function BaseEntityActionRuntimeUX_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || BaseEntityActionRuntimeUX)(); };
|
|
23
|
+
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: BaseEntityActionRuntimeUX, inputs: { Context: "Context" }, outputs: { Completed: "Completed", Cancelled: "Cancelled" } });
|
|
24
|
+
}
|
|
25
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(BaseEntityActionRuntimeUX, [{
|
|
26
|
+
type: Directive
|
|
27
|
+
}], null, { Context: [{
|
|
28
|
+
type: Input
|
|
29
|
+
}], Completed: [{
|
|
30
|
+
type: Output
|
|
31
|
+
}], Cancelled: [{
|
|
32
|
+
type: Output
|
|
33
|
+
}] }); })();
|
|
34
|
+
//# sourceMappingURL=base-entity-action-runtime-ux.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-entity-action-runtime-ux.js","sourceRoot":"","sources":["../../src/lib/base-entity-action-runtime-ux.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;;AAGvE;;;GAGG;AAEH,MAAM,OAAgB,yBAAyB;IAC3C,+FAA+F;IACtF,OAAO,CAAyB;IAEzC,sGAAsG;IAC5F,SAAS,GAAG,IAAI,YAAY,EAAwB,CAAC;IAE/D,kEAAkE;IACxD,SAAS,GAAG,IAAI,YAAY,EAAQ,CAAC;mHAR7B,yBAAyB;6DAAzB,yBAAyB;;iFAAzB,yBAAyB;cAD9C,SAAS;;kBAGL,KAAK;;kBAGL,MAAM;;kBAGN,MAAM","sourcesContent":["/**\n * @fileoverview The plugin contract for an entity-action runtime UX. A concrete driver is a standalone\n * Angular component that extends this base and registers itself with `@RegisterClass(BaseEntityActionRuntimeUX,\n * '<DriverKey>')`. The host ({@link EntityActionUXHostComponent}) resolves the key via the MJ ClassFactory,\n * mounts the component, hands it a {@link EntityActionUXContext}, calls {@link Start}, and listens for the\n * `Completed` / `Cancelled` outputs. New drivers ship in this package or any consumer — no host changes.\n * @module @memberjunction/ng-entity-action-ux\n */\nimport { Directive, EventEmitter, Input, Output } from '@angular/core';\nimport type { EntityActionUXContext, EntityActionUXResult } from './runtime-ux-context';\n\n/**\n * Abstract base for a runtime-UX driver. `@Directive()` (not `@Component`) so it can be a decorated,\n * DI-capable base class without its own template; concrete drivers supply the `@Component`.\n */\n@Directive()\nexport abstract class BaseEntityActionRuntimeUX {\n /** The work context, set by the host immediately after construction (before {@link Start}). */\n @Input() Context!: EntityActionUXContext;\n\n /** Emit when the driver has finished its work (applied or not — see {@link EntityActionUXResult}). */\n @Output() Completed = new EventEmitter<EntityActionUXResult>();\n\n /** Emit when the user dismisses the driver without completing. */\n @Output() Cancelled = new EventEmitter<void>();\n\n /**\n * Begin the driver's flow. Called once by the host after `Context` is set. Implementations typically\n * kick off an async preview/load here and drive their own template state from it.\n */\n abstract Start(): void | Promise<void>;\n}\n"]}
|