spiderly 19.8.7 → 19.8.8
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/agent/docs/angular-customization/{SKILL.md → index.md} +9 -0
- package/agent/docs/angular-customization/references/helper-functions.generated.md +1 -0
- package/agent/manifest.json +5 -0
- package/agent/skills/api-keys/SKILL.md +267 -0
- package/fesm2022/spiderly.mjs +42 -12
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +27 -1
- package/lib/services/helper-functions.d.ts +1 -0
- package/package.json +1 -1
- package/styles/components/spiderly-data-table/spiderly-data-table.component.scss +9 -0
- /package/agent/docs/authorization/{SKILL.md → index.md} +0 -0
- /package/agent/docs/backend-hooks/{SKILL.md → index.md} +0 -0
- /package/agent/docs/backend-localization/{SKILL.md → index.md} +0 -0
- /package/agent/docs/backend-testing/{SKILL.md → index.md} +0 -0
- /package/agent/docs/custom-endpoints/{SKILL.md → index.md} +0 -0
- /package/agent/docs/e2e-testing/{SKILL.md → index.md} +0 -0
- /package/agent/docs/entity-design/{SKILL.md → index.md} +0 -0
- /package/agent/docs/file-storage/{SKILL.md → index.md} +0 -0
- /package/agent/docs/filtering-patterns/{SKILL.md → index.md} +0 -0
- /package/agent/docs/frontend-localization/{SKILL.md → index.md} +0 -0
- /package/agent/docs/mapper-customization/{SKILL.md → index.md} +0 -0
|
@@ -182,6 +182,15 @@ cols: Column<ProductDTO>[] = [
|
|
|
182
182
|
|
|
183
183
|
The `onClick` callback receives an `ActionClickEvent` — `{ id, row, element, originalEvent }`. Use `element` (or `originalEvent`) to anchor an overlay/popover to the clicked action; `row` gives you the full row object. It fires only for custom `field` values (never `'Details'` / `'Delete'`).
|
|
184
184
|
|
|
185
|
+
For a click on the cell value itself (not an action icon), set a column's `onCellClick` callback — the mirror of `Action.onClick`, but for plain value cells:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
new Column({ field: 'total', name: 'Total', filterType: 'numeric',
|
|
189
|
+
onCellClick: (e) => this.itemsPopover.show(e.originalEvent, e.element) }),
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
It receives a `CellClickEvent` — `{ id, field, row, value, displayValue, element, originalEvent }`, where `value` is the raw cell value and `displayValue` is the formatted text shown. Only columns that set it become clickable (they get a `cursor`/hover affordance), and the click stops propagation so it does **not** also trigger `navigateOnRowClick`. Anchor overlays with `element` — it's the clicked `<td>`, captured synchronously, so it stays valid inside an async handler (`originalEvent.currentTarget` is null once dispatch ends). Not applied to editable cells.
|
|
193
|
+
|
|
185
194
|
### Key Inputs
|
|
186
195
|
|
|
187
196
|
| Input | Type | Default | Purpose |
|
|
@@ -32,6 +32,7 @@ Reusable helpers exported from `helper-functions.ts`. Import the one you need in
|
|
|
32
32
|
| `nameof(key1: any, key2?: any): any` | |
|
|
33
33
|
| `parseDateOnlyLocal(s: string): Date \| null` | |
|
|
34
34
|
| `pushAction(cols: Column[], action: Action)` | |
|
|
35
|
+
| `saveResponseAsFile(res: HttpResponse<Blob>, fallbackName: string): void` | |
|
|
35
36
|
| `selectedTab(tabs: SpiderlyTab[]): number` | |
|
|
36
37
|
| `singleOrDefault<T>(array: T[], predicate: (item: T) => boolean): T \| undefined` | |
|
|
37
38
|
| `splitPascalCase(input: string)` | |
|
package/agent/manifest.json
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
"surface": "skill",
|
|
6
6
|
"description": "Scaffold a new Spiderly entity end-to-end (entity class, Angular pages, routes, menu, migration)"
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
"name": "api-keys",
|
|
10
|
+
"surface": "skill",
|
|
11
|
+
"description": "Add the API-keys feature to a Spiderly app — per-key authentication via an X-Api-Key header, where each key is a first-class principal carrying its own roles. Use when a project needs machine/partner/agent access to its REST API (generate, scope-to-roles, expire, revoke). Opt-in; not part of the default app."
|
|
12
|
+
},
|
|
8
13
|
{
|
|
9
14
|
"name": "backups",
|
|
10
15
|
"surface": "skill",
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-keys
|
|
3
|
+
description: Add the API-keys feature to a Spiderly app — per-key authentication via an X-Api-Key header, where each key is a first-class principal carrying its own roles. Use when a project needs machine/partner/agent access to its REST API (generate, scope-to-roles, expire, revoke). Opt-in; not part of the default app.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add API Keys
|
|
7
|
+
|
|
8
|
+
This builds the API-keys feature into a Spiderly consumer app: external clients authenticate with an `X-Api-Key` header, and each key is a **first-class principal** (`PrincipalKinds.ApiKey`) that carries its **own roles** — it is not an impersonation of a user. Authorization resolves a key's permissions through the same principal pipeline as a user, so `[HasPermission]` endpoints accept either a JWT or a key with no per-endpoint change.
|
|
9
|
+
|
|
10
|
+
**This is opt-in.** API keys are not part of a default Spiderly app — nothing here exists until you run this skill, and an app without it has zero API-key surface. Don't add it unless the project actually needs machine/partner/agent API access.
|
|
11
|
+
|
|
12
|
+
## Why a key is a principal (read once)
|
|
13
|
+
|
|
14
|
+
An API key must be **individually revocable** and **separately expirable** — a stateless token (e.g. a long-lived JWT) can't be revoked without either rotating the signing key (logging out every user) or a per-request denylist. So a key is an opaque random secret, stored **hashed**, checked against a row on every request. The framework owns that mechanism (`Spiderly.Security`); this skill assembles the per-app pieces on top.
|
|
15
|
+
|
|
16
|
+
Modeling the key as its own `ISecurityPrincipal` (rather than "resolve to the owning user + cap permissions") means:
|
|
17
|
+
- The key's authority = the union of **its own** roles' permissions. A role-less key has **no** permissions.
|
|
18
|
+
- The owning `User` is just **management metadata** (who created/lists/revokes it), decoupled from authority.
|
|
19
|
+
- No runtime permission-cap machinery — authorization treats the key like any other principal.
|
|
20
|
+
|
|
21
|
+
## Step 0 — Prerequisites
|
|
22
|
+
|
|
23
|
+
1. **Spiderly version** — confirm the compiled mechanism is present. It must expose `Spiderly.Security.Authentication.IApiKeyAuthenticator`, `AddSpiderlyApiKeyAuthentication`, and `PrincipalKinds.ApiKey`. If a `using Spiderly.Security.Authentication;` doesn't resolve those, the app is on too old a Spiderly version — upgrade first (see the `spiderly-upgrade` skill).
|
|
24
|
+
2. **The app must already use Spiderly auth** — it registers a human principal (`AddSpiderlyPrincipal<User>(PrincipalKinds.User)`) and calls `spiderly.AddAuthentication()`. API keys add a *second* principal kind alongside it.
|
|
25
|
+
|
|
26
|
+
Decide with the user up front: **do they want an admin UI** to manage keys, or **API/agent-only** (drive generation via the REST endpoints / a management skill)? The backend is identical either way; the UI is the optional Step 8.
|
|
27
|
+
|
|
28
|
+
## Step 1 — The `ApiKey` entity (+ junction + navs)
|
|
29
|
+
|
|
30
|
+
Create the entity in the Business project's `Entities/`. Adapt the namespace and the `User`/`Role` types to the app.
|
|
31
|
+
|
|
32
|
+
```csharp
|
|
33
|
+
[Index(nameof(KeyHash), IsUnique = true)]
|
|
34
|
+
[SpiderlyEntity]
|
|
35
|
+
public class ApiKey : BusinessObject<long>, IApiKey
|
|
36
|
+
{
|
|
37
|
+
[UIDoNotGenerate]
|
|
38
|
+
[Required]
|
|
39
|
+
[StringLength(64, MinimumLength = 1)]
|
|
40
|
+
public string KeyHash { get; set; } // SHA-256 of the key; the plaintext is never stored
|
|
41
|
+
|
|
42
|
+
[Required]
|
|
43
|
+
[StringLength(100, MinimumLength = 1)]
|
|
44
|
+
[DisplayName]
|
|
45
|
+
public string Name { get; set; }
|
|
46
|
+
|
|
47
|
+
// Creator — the principal (any kind) that minted this key. Audit metadata, auto-stamped at creation.
|
|
48
|
+
// A soft reference (no FK): the creator can be any principal kind (User, ApiKey, ...), so it's stored
|
|
49
|
+
// as principal id + kind rather than a typed navigation.
|
|
50
|
+
[UIDoNotGenerate]
|
|
51
|
+
public long CreatedById { get; set; }
|
|
52
|
+
|
|
53
|
+
[UIDoNotGenerate]
|
|
54
|
+
[Required]
|
|
55
|
+
[StringLength(50, MinimumLength = 1)]
|
|
56
|
+
public string CreatedByPrincipalKind { get; set; }
|
|
57
|
+
|
|
58
|
+
public DateTime? ExpiresAt { get; set; }
|
|
59
|
+
|
|
60
|
+
public bool? IsRevoked { get; set; }
|
|
61
|
+
|
|
62
|
+
// ISecurityPrincipal: the key's authority is the union of its own roles' permissions (M2M, like User.Roles).
|
|
63
|
+
public virtual List<Role> Roles { get; } = new(); // M2M
|
|
64
|
+
IReadOnlyCollection<IRole> ISecurityPrincipal.Roles => Roles;
|
|
65
|
+
|
|
66
|
+
public bool? IsDisabled { get; set; }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`IApiKey` (which extends `ISecurityPrincipal`) is `Spiderly.Security.Interfaces`; `IRole` is `Spiderly.Shared.Interfaces`. The entity implements `IApiKey` so the framework's default authenticator (Step 2) reads it generically.
|
|
71
|
+
|
|
72
|
+
Add the M2M junction (mirrors the app's `UserRole`):
|
|
73
|
+
|
|
74
|
+
```csharp
|
|
75
|
+
[M2M]
|
|
76
|
+
[SpiderlyEntity]
|
|
77
|
+
public class ApiKeyRole
|
|
78
|
+
{
|
|
79
|
+
[M2MWithMany(nameof(ApiKey.Roles))]
|
|
80
|
+
public virtual ApiKey ApiKey { get; set; }
|
|
81
|
+
|
|
82
|
+
[M2MWithMany(nameof(Role.ApiKeys))]
|
|
83
|
+
public virtual Role Role { get; set; }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Add the inverse M2M nav `public virtual List<ApiKey> ApiKeys { get; } = new(); // M2M` on **`Role`**. There is **no** nav on `User` — the creator is a soft reference (id + kind), not a navigation.
|
|
88
|
+
|
|
89
|
+
## Step 2 — The authenticator (you don't write one)
|
|
90
|
+
|
|
91
|
+
The framework ships `DefaultApiKeyAuthenticator<TApiKey>`, which does the hash→active-id lookup over your `ApiKey` entity generically (it reads it via the `IApiKey` interface, so it stays decoupled from your namespace). You get it for free from the Step 4 registration — there is **no authenticator to hand-write**.
|
|
92
|
+
|
|
93
|
+
Only implement your own `IApiKeyAuthenticator` for a non-standard lookup (e.g. an external key store). If you do, register it **before** `AddSpiderlyApiKeyAuthentication<ApiKey>()` — the framework adds the default with `TryAdd`, so your registration wins.
|
|
94
|
+
|
|
95
|
+
## Step 3 — `ApiKeyService` hooks (generate + hash + show-once)
|
|
96
|
+
|
|
97
|
+
Extend the generated `ApiKeyServiceGenerated` so the admin/CRUD save path mints the key, stores only its hash, and returns the plaintext **exactly once**:
|
|
98
|
+
|
|
99
|
+
```csharp
|
|
100
|
+
[SpiderlyService]
|
|
101
|
+
public class ApiKeyService : ApiKeyServiceGenerated
|
|
102
|
+
{
|
|
103
|
+
private readonly AuthenticationService _authenticationService;
|
|
104
|
+
private readonly AuthorizationService _authorizationService; // the app's most-derived authorization service
|
|
105
|
+
private string _generatedPlainTextKey;
|
|
106
|
+
|
|
107
|
+
public ApiKeyService(EntityServiceDependencies deps, AuthenticationService authenticationService, AuthorizationService authorizationService) : base(deps)
|
|
108
|
+
{
|
|
109
|
+
_authenticationService = authenticationService;
|
|
110
|
+
_authorizationService = authorizationService;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
protected override async Task OnBeforeSaveApiKeyAndReturnMainUIFormDTO(ApiKeySaveBodyDTO saveBodyDTO)
|
|
114
|
+
{
|
|
115
|
+
if (saveBodyDTO.ApiKeyDTO.Id <= 0)
|
|
116
|
+
{
|
|
117
|
+
string plainTextKey = ApiKeyHelper.GenerateRandomKey();
|
|
118
|
+
saveBodyDTO.ApiKeyDTO.KeyHash = ApiKeyHelper.ComputeSha256Hash(plainTextKey);
|
|
119
|
+
_generatedPlainTextKey = plainTextKey;
|
|
120
|
+
saveBodyDTO.ApiKeyDTO.CreatedById = _authenticationService.GetCurrentUserId();
|
|
121
|
+
saveBodyDTO.ApiKeyDTO.CreatedByPrincipalKind = _authenticationService.GetCurrentPrincipalKind();
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
{
|
|
125
|
+
saveBodyDTO.ApiKeyDTO.KeyHash = await _deps.Context.DbSet<ApiKey>()
|
|
126
|
+
.AsNoTracking().Where(x => x.Id == saveBodyDTO.ApiKeyDTO.Id).Select(x => x.KeyHash).FirstAsync();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected override async Task OnAfterSaveApiKeyAndReturnMainUIFormDTO(ApiKeySaveBodyDTO saveBodyDTO, ApiKeyMainUIFormDTO mainUIFormDTO)
|
|
131
|
+
{
|
|
132
|
+
if (_generatedPlainTextKey != null)
|
|
133
|
+
{
|
|
134
|
+
mainUIFormDTO.PlainTextKey = _generatedPlainTextKey; // add this property via a partial ApiKeyMainUIFormDTO (Step 6)
|
|
135
|
+
_generatedPlainTextKey = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Guard EVERY role-assignment path, not just the Generate endpoint: an admin may only grant a key roles
|
|
140
|
+
// whose permissions they themselves hold. This is the M2M role-mutation method, so guarding it here covers
|
|
141
|
+
// the admin roles editor (and any future caller) by construction.
|
|
142
|
+
public override async Task UpdateRolesForApiKey(long id, List<int> selectedIds)
|
|
143
|
+
{
|
|
144
|
+
await _authorizationService.AuthorizeApiKeyRoleAssignmentAsync(selectedIds);
|
|
145
|
+
await base.UpdateRolesForApiKey(id, selectedIds);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`ApiKeyHelper` is `Spiderly.Security.Authentication`. If the app's authorization service has a different class name, inject that.
|
|
151
|
+
|
|
152
|
+
## Step 4 — Register the principal + the auth scheme
|
|
153
|
+
|
|
154
|
+
In the app's service-registration (where `AddSpiderlyPrincipal<User>` is called, **after** `AddSpiderly(...)`):
|
|
155
|
+
|
|
156
|
+
```csharp
|
|
157
|
+
services.AddSpiderlyPrincipal<ApiKey>(PrincipalKinds.ApiKey);
|
|
158
|
+
services.AddSpiderlyApiKeyAuthentication<ApiKey>();
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`AddSpiderlyApiKeyAuthentication` registers the `ApiKey` scheme **and** a forwarding policy scheme set as the default — so `X-Api-Key` requests route to the key handler and everything else to JWT, with no per-endpoint `[Authorize(AuthenticationSchemes=...)]` churn. Registering a second principal kind makes the app multi-principal; the JWT login already stamps `PrincipalKinds.User`, the key handler stamps `PrincipalKinds.ApiKey`, so both satisfy the now-required `principal_kind` claim.
|
|
162
|
+
|
|
163
|
+
## Step 5 — The issuance guard (authorization service)
|
|
164
|
+
|
|
165
|
+
In the app's authorization service (the `[SpiderlyService]` extending `AuthorizationServiceGenerated`), add the guard the `UpdateRolesForApiKey` override and the Generate endpoint both call:
|
|
166
|
+
|
|
167
|
+
```csharp
|
|
168
|
+
/// <summary>
|
|
169
|
+
/// Issuance guard: the principal creating a key may only grant it roles whose permissions that principal
|
|
170
|
+
/// already holds, so a key can never be minted more powerful than its creator. The creator can be any
|
|
171
|
+
/// principal kind (a user, a service account, another key), so the ceiling is resolved principal-agnostically
|
|
172
|
+
/// via GetCurrentPrincipalPermissionCodesAsync() (not the User-typed GetCurrentUserPermissionCodes). No
|
|
173
|
+
/// runtime cap — a key authorizes through its own roles — so this is enforced once, at assignment.
|
|
174
|
+
/// </summary>
|
|
175
|
+
public async Task AuthorizeApiKeyRoleAssignmentAsync(List<int> roleIds)
|
|
176
|
+
{
|
|
177
|
+
if (roleIds == null || roleIds.Count == 0)
|
|
178
|
+
return;
|
|
179
|
+
|
|
180
|
+
HashSet<string> creatorPermissions = (await GetCurrentPrincipalPermissionCodesAsync()).ToHashSet();
|
|
181
|
+
|
|
182
|
+
List<string> targetRolePermissions = await _context.DbSet<Role>()
|
|
183
|
+
.AsNoTracking()
|
|
184
|
+
.Where(r => roleIds.Contains(r.Id))
|
|
185
|
+
.SelectMany(r => r.Permissions)
|
|
186
|
+
.Select(p => p.Code)
|
|
187
|
+
.Distinct()
|
|
188
|
+
.ToListAsync();
|
|
189
|
+
|
|
190
|
+
if (!targetRolePermissions.All(p => creatorPermissions.Contains(p)))
|
|
191
|
+
throw new BusinessException(_localizer["ApiKeyRoleExceedsYourPermissionsException"]);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Add the `ApiKeyRoleExceedsYourPermissionsException` translation key (see the `backend-localization` doc).
|
|
196
|
+
|
|
197
|
+
## Step 6 — Generate / Revoke endpoints + DTOs
|
|
198
|
+
|
|
199
|
+
These are the machine-facing REST surface (also what a management skill drives). Add to a `[SpiderlyController]` (e.g. the security controller). DTOs go in the Admin DTO folder; the `PlainTextKey` extension is a **partial** of the generated `ApiKeyMainUIFormDTO` and must use the bare `...Business.DTO` namespace (see the `entity-design` doc on partial-DTO namespacing).
|
|
200
|
+
|
|
201
|
+
```csharp
|
|
202
|
+
// GenerateApiKeyRequestDTO: string Name; int? ExpiresInDays; [Required] List<int> RoleIds = new();
|
|
203
|
+
// GenerateApiKeyResponseDTO: string Key; string Name; DateTime? ExpiresAt; [Required] List<int> RoleIds = new(); [Required] List<string> RoleNames = new();
|
|
204
|
+
// RevokeApiKeyRequestDTO: [Required] long ApiKeyId;
|
|
205
|
+
// partial ApiKeyMainUIFormDTO (bare DTO namespace): [UIDoNotGenerate][StringLength(64)] string PlainTextKey;
|
|
206
|
+
|
|
207
|
+
[HttpPost, AuthGuard]
|
|
208
|
+
public async Task<GenerateApiKeyResponseDTO> GenerateApiKey([FromBody] GenerateApiKeyRequestDTO request)
|
|
209
|
+
{
|
|
210
|
+
await _authorizationService.AuthorizeAndThrowAsync(PermissionCodes.InsertApiKey); // principal-agnostic — any principal kind can create a key
|
|
211
|
+
long currentUserId = _authenticationService.GetCurrentUserId();
|
|
212
|
+
|
|
213
|
+
List<Role> roles = new();
|
|
214
|
+
if (request.RoleIds != null && request.RoleIds.Count > 0)
|
|
215
|
+
{
|
|
216
|
+
roles = await _context.DbSet<Role>().Where(x => request.RoleIds.Contains(x.Id)).ToListAsync();
|
|
217
|
+
List<int> missing = request.RoleIds.Except(roles.Select(r => r.Id)).ToList();
|
|
218
|
+
if (missing.Count > 0) throw new BusinessException($"Role(s) not found: {string.Join(", ", missing)}.");
|
|
219
|
+
await _authorizationService.AuthorizeApiKeyRoleAssignmentAsync(request.RoleIds);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
string plainTextKey = ApiKeyHelper.GenerateRandomKey();
|
|
223
|
+
DateTime? expiresAt = request.ExpiresInDays.HasValue ? DateTime.UtcNow.AddDays(request.ExpiresInDays.Value) : null;
|
|
224
|
+
|
|
225
|
+
ApiKey apiKey = new() { KeyHash = ApiKeyHelper.ComputeSha256Hash(plainTextKey), Name = request.Name,
|
|
226
|
+
CreatedById = currentUserId, CreatedByPrincipalKind = _authenticationService.GetCurrentPrincipalKind(),
|
|
227
|
+
ExpiresAt = expiresAt, IsRevoked = false };
|
|
228
|
+
apiKey.Roles.AddRange(roles);
|
|
229
|
+
_context.DbSet<ApiKey>().Add(apiKey);
|
|
230
|
+
await _context.SaveChangesAsync();
|
|
231
|
+
|
|
232
|
+
return new GenerateApiKeyResponseDTO { Key = plainTextKey, Name = request.Name, ExpiresAt = expiresAt,
|
|
233
|
+
RoleIds = roles.Select(r => r.Id).ToList(), RoleNames = roles.Select(r => r.Name).ToList() };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
[HttpPost, AuthGuard]
|
|
237
|
+
public async Task RevokeApiKey([FromBody] RevokeApiKeyRequestDTO request)
|
|
238
|
+
{
|
|
239
|
+
await _authorizationService.AuthorizeAndThrowAsync(PermissionCodes.UpdateApiKey);
|
|
240
|
+
ApiKey apiKey = await _context.DbSet<ApiKey>().FirstOrDefaultAsync(x => x.Id == request.ApiKeyId)
|
|
241
|
+
?? throw new BusinessException("API key not found.");
|
|
242
|
+
apiKey.IsRevoked = true;
|
|
243
|
+
await _context.SaveChangesAsync();
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Step 7 — Seed the `ApiKey` permissions + migrate
|
|
248
|
+
|
|
249
|
+
1. **Seed permissions** so the `PermissionCodes.{Read,Insert,Update,Delete}ApiKey` constants generate — add `Permission` seed rows (codes `ReadApiKey`/`InsertApiKey`/`UpdateApiKey`/`DeleteApiKey`) in the app's DbContext seed data, alongside the existing User/Role permission seeds, and grant them to the admin role.
|
|
250
|
+
2. **Migration** — add + apply an EF migration for the new `ApiKey` + `ApiKeyRole` tables (see the `ef-migrations` skill). If you're converting an *existing* single-`Role` API-key table to this M2M model, make the migration **data-preserving**: create the junction first, `INSERT ... SELECT` the old `RoleId`s into it, then drop the old column.
|
|
251
|
+
|
|
252
|
+
## Step 8 — (Optional) admin UI
|
|
253
|
+
|
|
254
|
+
Only if the user wants a clickable admin surface (otherwise skip — keys are managed via the endpoints / a management skill). Scaffold the list + details pages for the `ApiKey` entity (see the `add-entity` skill for pages/routes/menu), then customize the details page to show the generated key **once**: intercept the save response, and if it carries `plainTextKey`, show it in a read-only "copy this — it won't be shown again" dialog and suppress the auto-reroute until the dialog closes. A roles editor on the form routes through `UpdateRolesForApiKey`, which Step 3 already guards.
|
|
255
|
+
|
|
256
|
+
To keep the feature **headless** instead, add `[UIDoNotGenerate]` to the `ApiKey` class and don't create pages.
|
|
257
|
+
|
|
258
|
+
## Step 9 — Verify
|
|
259
|
+
|
|
260
|
+
Build the backend (`dotnet build` the Business project, then the WebAPI project). Then sanity-check the flow: generate a key (admin JWT) → call any `[HasPermission]` endpoint the key's roles cover with `X-Api-Key: <key>` → revoke it → confirm the same call now returns 401.
|
|
261
|
+
|
|
262
|
+
## Invariants to preserve
|
|
263
|
+
|
|
264
|
+
- **Store only the hash.** The plaintext is returned once at generation and never persisted.
|
|
265
|
+
- **Guard every role-assignment path.** Both the Generate endpoint and `UpdateRolesForApiKey` call `AuthorizeApiKeyRoleAssignmentAsync` — a key can never be minted more powerful than its creator.
|
|
266
|
+
- **A role-less key has no permissions** (deny), by design. Assign an admin role for a full-access key.
|
|
267
|
+
- **Revocation/expiry/disabled are checked live** in `DefaultApiKeyAuthenticator` on every request — that's the whole point of opaque keys over JWTs.
|
package/fesm2022/spiderly.mjs
CHANGED
|
@@ -626,10 +626,16 @@ function isExcelFileType(mimeType) {
|
|
|
626
626
|
}
|
|
627
627
|
return false;
|
|
628
628
|
}
|
|
629
|
+
function saveResponseAsFile(res, fallbackName) {
|
|
630
|
+
if (res?.body == null) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
let fileName = getFileNameFromContentDisposition(res, fallbackName);
|
|
634
|
+
FileSaver.saveAs(res.body, decodeURIComponent(fileName));
|
|
635
|
+
}
|
|
629
636
|
function exportListToExcel(exportListToExcelObservableMethod, filter) {
|
|
630
637
|
exportListToExcelObservableMethod(filter).subscribe((res) => {
|
|
631
|
-
|
|
632
|
-
FileSaver.saveAs(res.body, decodeURIComponent(fileName));
|
|
638
|
+
saveResponseAsFile(res, 'ExcelExport.xlsx');
|
|
633
639
|
});
|
|
634
640
|
}
|
|
635
641
|
function getPrimengNamebookOptions(namebookList) {
|
|
@@ -4161,6 +4167,34 @@ class SpiderlyDataTableComponent {
|
|
|
4161
4167
|
return;
|
|
4162
4168
|
this.navigateToDetails(row.id);
|
|
4163
4169
|
}
|
|
4170
|
+
/*
|
|
4171
|
+
* Handle a cell click. Only columns that opt in (have an `onCellClick` callback) react; for them
|
|
4172
|
+
* we stop propagation so the click does not also trigger row navigation, then invoke the consumer
|
|
4173
|
+
* callback with a fully-populated CellClickEvent. Inert cells fall through to onRowClick unchanged.
|
|
4174
|
+
*/
|
|
4175
|
+
onCellClick(col, rowData, event) {
|
|
4176
|
+
if (!col.onCellClick)
|
|
4177
|
+
return;
|
|
4178
|
+
event.stopPropagation();
|
|
4179
|
+
col.onCellClick({
|
|
4180
|
+
...this.buildClickEvent(rowData, event),
|
|
4181
|
+
field: col.field,
|
|
4182
|
+
value: rowData[col.field],
|
|
4183
|
+
displayValue: this.getRowData(rowData, col),
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
/*
|
|
4187
|
+
* Build the base click payload shared by custom action clicks and cell clicks. The clicked element
|
|
4188
|
+
* is captured here, not read later from `originalEvent.currentTarget` (which nulls after dispatch).
|
|
4189
|
+
*/
|
|
4190
|
+
buildClickEvent(rowData, event) {
|
|
4191
|
+
return {
|
|
4192
|
+
id: rowData[this.idField],
|
|
4193
|
+
row: rowData,
|
|
4194
|
+
element: event.currentTarget,
|
|
4195
|
+
originalEvent: event,
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4164
4198
|
deleteObject(rowId) {
|
|
4165
4199
|
this.openDeleteConfirmation({
|
|
4166
4200
|
deleteItemFromTableObservableMethod: this.deleteItemFromTableObservableMethod,
|
|
@@ -4253,12 +4287,7 @@ class SpiderlyDataTableComponent {
|
|
|
4253
4287
|
case 'Delete':
|
|
4254
4288
|
return this.deleteObject(rowData[this.idField]);
|
|
4255
4289
|
default:
|
|
4256
|
-
return action.onClick(
|
|
4257
|
-
id: rowData[this.idField],
|
|
4258
|
-
row: rowData,
|
|
4259
|
-
element: event.currentTarget,
|
|
4260
|
-
originalEvent: event,
|
|
4261
|
-
});
|
|
4290
|
+
return action.onClick(this.buildClickEvent(rowData, event));
|
|
4262
4291
|
}
|
|
4263
4292
|
}
|
|
4264
4293
|
getRowData(rowData, col) {
|
|
@@ -4415,7 +4444,7 @@ class SpiderlyDataTableComponent {
|
|
|
4415
4444
|
}
|
|
4416
4445
|
}
|
|
4417
4446
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, deps: [{ token: i3$2.Router }, { token: i1$5.DialogService }, { token: i3$2.ActivatedRoute }, { token: SpiderlyMessageService }, { token: i1.TranslocoService }, { token: ConfigServiceBase }, { token: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4418
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyDataTableComponent, isStandalone: true, selector: "spiderly-data-table", inputs: { tableTitle: "tableTitle", tableIcon: "tableIcon", items: "items", rows: "rows", cols: "cols", showPaginator: "showPaginator", showCardWrapper: "showCardWrapper", readonly: "readonly", idField: "idField", getPaginatedListObservableMethod: "getPaginatedListObservableMethod", exportListToExcelObservableMethod: "exportListToExcelObservableMethod", deleteItemFromTableObservableMethod: "deleteItemFromTableObservableMethod", deleteListFromTableObservableMethod: "deleteListFromTableObservableMethod", newlySelectedItems: "newlySelectedItems", unselectedItems: "unselectedItems", selectionMode: "selectionMode", selectedLazyLoadObservableMethod: "selectedLazyLoadObservableMethod", additionalFilterIdLong: "additionalFilterIdLong", showAddButton: "showAddButton", showExportToExcelButton: "showExportToExcelButton", showReloadTableButton: "showReloadTableButton", getFormArrayItems: "getFormArrayItems", hasLazyLoad: "hasLazyLoad", stateKey: "stateKey", stateStorage: "stateStorage", getAlreadySelectedItemIds: "getAlreadySelectedItemIds", getAlreadySelectedItems: "getAlreadySelectedItems", getFormControl: "getFormControl", additionalIndexes: "additionalIndexes", navigateOnRowClick: "navigateOnRowClick", rowNavigationPath: "rowNavigationPath" }, outputs: { onTotalRecordsChange: "onTotalRecordsChange", onLazyLoad: "onLazyLoad", onIsAllSelectedChange: "onIsAllSelectedChange", onRowSelect: "onRowSelect", onRowUnselect: "onRowUnselect" }, viewQueries: [{ propertyName: "table", first: true, predicate: ["dt"], descendants: true }], ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n [class]=\"\n showCardWrapper ? 'card responsive-card-padding overflow-auto' : ''\n \"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"items === undefined || loading === true\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n sortMode=\"multiple\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <button\n pButton\n [label]=\"t('ClearFilters')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-filter-slash\"\n (click)=\"clear(dt)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <th style=\"width: 0rem\" *ngIf=\"selectionMode == 'multiple'\">\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n ({{ rowsSelectedNumber }})\n </div>\n </th>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <th\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"col.sortable === false || !col.field\"\n [style]=\"getColHeaderWidth(col.filterType)\"\n >\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"col.sortable !== false && col.field\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n </span>\n <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"col.filterField ?? col.field\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [matchModeOptions]=\"getColMatchModeOptions(col.filterType)\"\n [matchMode]=\"getColMatchMode(col.filterType)\"\n >\n <ng-template\n *ngIf=\"isDropOrMulti(col.filterType)\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"col.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '240px' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n <ng-template\n *ngIf=\"col.filterType == 'date'\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-datepicker\n [ngModel]=\"value\"\n [showTime]=\"col.showTime\"\n (onSelect)=\"filter($event)\"\n ></p-datepicker>\n </ng-template>\n </p-columnFilter>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData)\"\n >\n <td *ngIf=\"selectionMode == 'multiple'\">\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <td [style]=\"getStyleForBodyColumn(col)\" *ngIf=\"!col.editable\">\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action)\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"getRowData(rowData, col)\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n {{ getRowData(rowData, col) }}\n </ng-container>\n </td>\n <td *ngIf=\"col.editable\">\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"loadingbody\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"Loading\") }}...\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: SpiderlyNumberComponent, selector: "spiderly-number", inputs: ["prefix", "showButtons", "decimal", "maxFractionDigits"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i10.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "virtualRowHeight", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i10.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i10.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "component", type: i10.ColumnFilter, selector: "p-columnFilter", inputs: ["field", "type", "display", "showMenu", "matchMode", "operator", "showOperator", "showClearButton", "showApplyButton", "showMatchModes", "showAddButton", "hideOnClear", "placeholder", "matchModeOptions", "maxConstraints", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "locale", "localeMatcher", "currency", "currencyDisplay", "useGrouping", "showButtons", "ariaLabel", "filterButtonProps"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i12.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i4$5.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "fluid", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "size", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i4$1.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "style", "styleClass", "inputStyle", "inputId", "name", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "disabled", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "fluid", "icon", "appendTo", "readonlyInput", "shortYearCutoff", "monthNavigator", "yearNavigator", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "required", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "variant", "size", "minDate", "maxDate", "disabledDates", "disabledDays", "yearRange", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "locale", "view", "defaultDate"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
4447
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.13", type: SpiderlyDataTableComponent, isStandalone: true, selector: "spiderly-data-table", inputs: { tableTitle: "tableTitle", tableIcon: "tableIcon", items: "items", rows: "rows", cols: "cols", showPaginator: "showPaginator", showCardWrapper: "showCardWrapper", readonly: "readonly", idField: "idField", getPaginatedListObservableMethod: "getPaginatedListObservableMethod", exportListToExcelObservableMethod: "exportListToExcelObservableMethod", deleteItemFromTableObservableMethod: "deleteItemFromTableObservableMethod", deleteListFromTableObservableMethod: "deleteListFromTableObservableMethod", newlySelectedItems: "newlySelectedItems", unselectedItems: "unselectedItems", selectionMode: "selectionMode", selectedLazyLoadObservableMethod: "selectedLazyLoadObservableMethod", additionalFilterIdLong: "additionalFilterIdLong", showAddButton: "showAddButton", showExportToExcelButton: "showExportToExcelButton", showReloadTableButton: "showReloadTableButton", getFormArrayItems: "getFormArrayItems", hasLazyLoad: "hasLazyLoad", stateKey: "stateKey", stateStorage: "stateStorage", getAlreadySelectedItemIds: "getAlreadySelectedItemIds", getAlreadySelectedItems: "getAlreadySelectedItems", getFormControl: "getFormControl", additionalIndexes: "additionalIndexes", navigateOnRowClick: "navigateOnRowClick", rowNavigationPath: "rowNavigationPath" }, outputs: { onTotalRecordsChange: "onTotalRecordsChange", onLazyLoad: "onLazyLoad", onIsAllSelectedChange: "onIsAllSelectedChange", onRowSelect: "onRowSelect", onRowUnselect: "onRowUnselect" }, viewQueries: [{ propertyName: "table", first: true, predicate: ["dt"], descendants: true }], ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <div\n [class]=\"\n showCardWrapper ? 'card responsive-card-padding overflow-auto' : ''\n \"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"items === undefined || loading === true\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n sortMode=\"multiple\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <button\n pButton\n [label]=\"t('ClearFilters')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-filter-slash\"\n (click)=\"clear(dt)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <th style=\"width: 0rem\" *ngIf=\"selectionMode == 'multiple'\">\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n ({{ rowsSelectedNumber }})\n </div>\n </th>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <th\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"col.sortable === false || !col.field\"\n [style]=\"getColHeaderWidth(col.filterType)\"\n >\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"col.sortable !== false && col.field\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n </span>\n <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"col.filterField ?? col.field\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [matchModeOptions]=\"getColMatchModeOptions(col.filterType)\"\n [matchMode]=\"getColMatchMode(col.filterType)\"\n >\n <ng-template\n *ngIf=\"isDropOrMulti(col.filterType)\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"col.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '240px' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n <ng-template\n *ngIf=\"col.filterType == 'date'\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-datepicker\n [ngModel]=\"value\"\n [showTime]=\"col.showTime\"\n (onSelect)=\"filter($event)\"\n ></p-datepicker>\n </ng-template>\n </p-columnFilter>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData)\"\n >\n <td *ngIf=\"selectionMode == 'multiple'\">\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action)\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"getRowData(rowData, col)\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n {{ getRowData(rowData, col) }}\n </ng-container>\n </td>\n <td *ngIf=\"col.editable\">\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"loadingbody\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"Loading\") }}...\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: SpiderlyControlsModule }, { kind: "component", type: SpiderlyButtonComponent, selector: "spiderly-button", inputs: ["type"] }, { kind: "component", type: SpiderlyNumberComponent, selector: "spiderly-number", inputs: ["prefix", "showButtons", "decimal", "maxFractionDigits"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i10.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "virtualRowHeight", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i1$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i10.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i10.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "component", type: i10.ColumnFilter, selector: "p-columnFilter", inputs: ["field", "type", "display", "showMenu", "matchMode", "operator", "showOperator", "showClearButton", "showApplyButton", "showMatchModes", "showAddButton", "hideOnClear", "placeholder", "matchModeOptions", "maxConstraints", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "locale", "localeMatcher", "currency", "currencyDisplay", "useGrouping", "showButtons", "ariaLabel", "filterButtonProps"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i12.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "fluid", "label", "icon", "buttonProps"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i4$5.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "fluid", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "size", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i4$1.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "style", "styleClass", "inputStyle", "inputId", "name", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "disabled", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "fluid", "icon", "appendTo", "readonlyInput", "shortYearCutoff", "monthNavigator", "yearNavigator", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "required", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "variant", "size", "minDate", "maxDate", "disabledDates", "disabledDays", "yearRange", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "locale", "view", "defaultDate"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["value", "name", "disabled", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "inputStyle", "styleClass", "inputClass", "indeterminate", "size", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }] }); }
|
|
4419
4448
|
}
|
|
4420
4449
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImport: i0, type: SpiderlyDataTableComponent, decorators: [{
|
|
4421
4450
|
type: Component,
|
|
@@ -4430,7 +4459,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4430
4459
|
DatePickerModule,
|
|
4431
4460
|
CheckboxModule,
|
|
4432
4461
|
TooltipModule,
|
|
4433
|
-
], template: "<ng-container *transloco=\"let t\">\n <div\n [class]=\"\n showCardWrapper ? 'card responsive-card-padding overflow-auto' : ''\n \"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"items === undefined || loading === true\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n sortMode=\"multiple\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <button\n pButton\n [label]=\"t('ClearFilters')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-filter-slash\"\n (click)=\"clear(dt)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <th style=\"width: 0rem\" *ngIf=\"selectionMode == 'multiple'\">\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n ({{ rowsSelectedNumber }})\n </div>\n </th>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <th\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"col.sortable === false || !col.field\"\n [style]=\"getColHeaderWidth(col.filterType)\"\n >\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"col.sortable !== false && col.field\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n </span>\n <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"col.filterField ?? col.field\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [matchModeOptions]=\"getColMatchModeOptions(col.filterType)\"\n [matchMode]=\"getColMatchMode(col.filterType)\"\n >\n <ng-template\n *ngIf=\"isDropOrMulti(col.filterType)\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"col.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '240px' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n <ng-template\n *ngIf=\"col.filterType == 'date'\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-datepicker\n [ngModel]=\"value\"\n [showTime]=\"col.showTime\"\n (onSelect)=\"filter($event)\"\n ></p-datepicker>\n </ng-template>\n </p-columnFilter>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData)\"\n >\n <td *ngIf=\"selectionMode == 'multiple'\">\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <td [style]=\"getStyleForBodyColumn(col)\" *ngIf=\"!col.editable\">\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action)\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"getRowData(rowData, col)\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n {{ getRowData(rowData, col) }}\n </ng-container>\n </td>\n <td *ngIf=\"col.editable\">\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"loadingbody\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"Loading\") }}...\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"] }]
|
|
4462
|
+
], template: "<ng-container *transloco=\"let t\">\n <div\n [class]=\"\n showCardWrapper ? 'card responsive-card-padding overflow-auto' : ''\n \"\n >\n <p-table\n #dt\n [value]=\"items\"\n [rows]=\"rows\"\n [rowHover]=\"true\"\n [paginator]=\"showPaginator\"\n responsiveLayout=\"scroll\"\n [lazy]=\"hasLazyLoad\"\n (onLazyLoad)=\"lazyLoad($event)\"\n [totalRecords]=\"totalRecords\"\n class=\"spiderly-table\"\n [loading]=\"items === undefined || loading === true\"\n [selectionMode]=\"selectionMode\"\n dataKey=\"id\"\n (onFilter)=\"filter($event)\"\n sortMode=\"multiple\"\n [stateKey]=\"resolvedStateKey\"\n [stateStorage]=\"stateStorage\"\n >\n <ng-template pTemplate=\"caption\">\n <div class=\"table-header overflow-auto\">\n <div style=\"display: flex; align-items: center; gap: 8px\">\n <i class=\"{{ tableIcon }}\" style=\"font-size: 20px\"></i>\n <div style=\"margin: 0px; font-size: 17.5px\">{{ tableTitle }}</div>\n </div>\n <div style=\"display: flex; gap: 8px\">\n <button\n pButton\n [label]=\"t('ClearFilters')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-filter-slash\"\n (click)=\"clear(dt)\"\n ></button>\n <button\n pButton\n *ngIf=\"showExportToExcelButton\"\n [label]=\"t('ExportToExcel')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-download\"\n (click)=\"exportListToExcel()\"\n ></button>\n <button\n pButton\n *ngIf=\"showReloadTableButton\"\n [label]=\"t('Reload')\"\n class=\"p-button-outlined\"\n style=\"flex: none\"\n icon=\"pi pi-refresh\"\n (click)=\"reload()\"\n ></button>\n <button\n pButton\n *ngIf=\"deleteListFromTableObservableMethod && rowsSelectedNumber > 0\"\n [label]=\"t('DeleteSelected') + ' (' + rowsSelectedNumber + ')'\"\n class=\"p-button-danger\"\n style=\"flex: none\"\n icon=\"pi pi-trash\"\n (click)=\"deleteSelectedObjects()\"\n ></button>\n </div>\n </div>\n </ng-template>\n <ng-template pTemplate=\"header\">\n <tr>\n <th style=\"width: 0rem\" *ngIf=\"selectionMode == 'multiple'\">\n <div style=\"display: flex; gap: 8px\">\n <p-checkbox\n *ngIf=\"showSelectAllCheckbox\"\n [disabled]=\"readonly\"\n (onChange)=\"selectAll($event.checked)\"\n [(ngModel)]=\"fakeIsAllSelected\"\n [binary]=\"true\"\n ></p-checkbox>\n ({{ rowsSelectedNumber }})\n </div>\n </th>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <th\n [pSortableColumn]=\"col.field\"\n [pSortableColumnDisabled]=\"col.sortable === false || !col.field\"\n [style]=\"getColHeaderWidth(col.filterType)\"\n >\n <div\n style=\"\n display: flex;\n justify-content: space-between;\n align-items: center;\n \"\n >\n <span style=\"display: flex; align-items: center; gap: 4px\">\n {{ col.name }}\n <p-sortIcon\n *ngIf=\"col.sortable !== false && col.field\"\n [field]=\"col.field!\"\n ></p-sortIcon>\n </span>\n <p-columnFilter\n *ngIf=\"col.filterType != null && col.filterType !== 'blob'\"\n [type]=\"col.filterType\"\n [field]=\"col.filterField ?? col.field\"\n display=\"menu\"\n [placeholder]=\"col.filterPlaceholder\"\n [showOperator]=\"false\"\n [showMatchModes]=\"col.showMatchModes\"\n [showAddButton]=\"col.showAddButton\"\n [matchModeOptions]=\"getColMatchModeOptions(col.filterType)\"\n [matchMode]=\"getColMatchMode(col.filterType)\"\n >\n <ng-template\n *ngIf=\"isDropOrMulti(col.filterType)\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-multiSelect\n [ngModel]=\"value\"\n [options]=\"col.dropdownOrMultiselectValues\"\n [placeholder]=\"t('All')\"\n (onChange)=\"filter($event.value)\"\n optionLabel=\"label\"\n optionValue=\"code\"\n [style]=\"{ width: '240px' }\"\n >\n <ng-template let-item pTemplate=\"item\">\n <div class=\"p-multiselect-representative-option\">\n <span class=\"ml-2\">{{ item.label }}</span>\n </div>\n </ng-template>\n </p-multiSelect>\n </ng-template>\n <ng-template\n *ngIf=\"col.filterType == 'date'\"\n pTemplate=\"filter\"\n let-value\n let-filter=\"filterCallback\"\n >\n <p-datepicker\n [ngModel]=\"value\"\n [showTime]=\"col.showTime\"\n (onSelect)=\"filter($event)\"\n ></p-datepicker>\n </ng-template>\n </p-columnFilter>\n </div>\n </th>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template\n pTemplate=\"body\"\n let-rowData\n let-index=\"rowIndex\"\n let-editing=\"editing\"\n >\n <tr\n [class.clickable]=\"navigateOnRowClick\"\n (click)=\"onRowClick(rowData)\"\n >\n <td *ngIf=\"selectionMode == 'multiple'\">\n <p-checkbox\n [disabled]=\"readonly\"\n (onChange)=\"selectRow(rowData[idField], rowData.index)\"\n [ngModel]=\"isRowSelected(rowData[idField])\"\n [binary]=\"true\"\n ></p-checkbox>\n </td>\n <ng-container *ngFor=\"let col of cols; trackBy: colTrackByFn\">\n <td\n [style]=\"getStyleForBodyColumn(col)\"\n [class.clickable]=\"col.onCellClick\"\n (click)=\"onCellClick(col, rowData, $event)\"\n *ngIf=\"!col.editable\"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 18px;\n \"\n >\n <ng-container\n *ngFor=\"let action of col.actions; trackBy: actionTrackByFn\"\n >\n <span\n [pTooltip]=\"action.name\"\n [class]=\"getClassForAction(action)\"\n [style]=\"getStyleForAction(action)\"\n (click)=\"getMethodForAction(action, rowData, $event)\"\n ></span>\n </ng-container>\n </div>\n <ng-container *ngIf=\"col.filterType === 'blob'\">\n <img width=\"45\" [src]=\"getRowData(rowData, col)\" alt=\"\" />\n </ng-container>\n <ng-container *ngIf=\"col.filterType !== 'blob'\">\n {{ getRowData(rowData, col) }}\n </ng-container>\n </td>\n <td *ngIf=\"col.editable\">\n <spiderly-number\n [control]=\"getFormArrayControlByIndex(col.field, rowData.index)\"\n [showLabel]=\"false\"\n ></spiderly-number>\n </td>\n </ng-container>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"emptymessage\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"NoRecordsFound\") }}\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"loadingbody\">\n <tr>\n <td\n [attr.colspan]=\"\n cols?.length + (selectionMode === 'multiple' ? 1 : 0)\n \"\n >\n {{ t(\"Loading\") }}...\n </td>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"paginatorleft\">\n {{ t(\"TotalRecords\") }}: {{ totalRecords }}\n </ng-template>\n <ng-template pTemplate=\"paginatorright\">\n <div style=\"display: flex; justify-content: end; gap: 10px\">\n <spiderly-button\n *ngIf=\"showAddButton\"\n [label]=\"t('AddNew')\"\n icon=\"pi pi-plus\"\n (onClick)=\"navigateToDetails(0)\"\n ></spiderly-button>\n </div>\n </ng-template>\n </p-table>\n </div>\n</ng-container>\n", styles: [".table-header{display:flex;justify-content:space-between;align-items:center}@media (max-width: 640px){.table-header{display:flex;flex-direction:column;align-items:start;gap:14px}}.spiderly-table .p-paginator{padding:1rem}@media (min-width: 1400px){.spiderly-table .p-paginator-left-content{position:absolute;padding:14px;left:0}}@media (min-width: 1400px){.spiderly-table .p-paginator-right-content{position:absolute;padding:14px;right:0}}:host ::ng-deep .clickable{cursor:pointer}:host ::ng-deep td.clickable:hover{text-decoration:underline}:host ::ng-deep .p-datatable-thead{position:unset!important}:host ::ng-deep .p-datatable-header{border-radius:6px 6px 0 0}:host ::ng-deep .p-datatable{border-radius:var(--p-content-border-radius);border:1px solid var(--p-datatable-border-color)}\n"] }]
|
|
4434
4463
|
}], ctorParameters: () => [{ type: i3$2.Router }, { type: i1$5.DialogService }, { type: i3$2.ActivatedRoute }, { type: SpiderlyMessageService }, { type: i1.TranslocoService }, { type: ConfigServiceBase }, { type: undefined, decorators: [{
|
|
4435
4464
|
type: Inject,
|
|
4436
4465
|
args: [LOCALE_ID]
|
|
@@ -4521,7 +4550,7 @@ class Action {
|
|
|
4521
4550
|
}
|
|
4522
4551
|
}
|
|
4523
4552
|
class Column {
|
|
4524
|
-
constructor({ name, field, filterField, filterType, filterPlaceholder, showMatchModes, showAddButton, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, } = {}) {
|
|
4553
|
+
constructor({ name, field, filterField, filterType, filterPlaceholder, showMatchModes, showAddButton, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, onCellClick, } = {}) {
|
|
4525
4554
|
this.name = name;
|
|
4526
4555
|
this.field = field;
|
|
4527
4556
|
this.filterField = filterField;
|
|
@@ -4535,6 +4564,7 @@ class Column {
|
|
|
4535
4564
|
this.showTime = showTime;
|
|
4536
4565
|
this.decimalPlaces = decimalPlaces;
|
|
4537
4566
|
this.sortable = sortable;
|
|
4567
|
+
this.onCellClick = onCellClick;
|
|
4538
4568
|
}
|
|
4539
4569
|
}
|
|
4540
4570
|
class RowClickEvent {
|
|
@@ -5209,5 +5239,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
5209
5239
|
* Generated bundle index. Do not edit.
|
|
5210
5240
|
*/
|
|
5211
5241
|
|
|
5212
|
-
export { Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, FooterComponent, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, capitalizeFirstChar, deleteAction, exportListToExcel, firstCharToUpper, getFileNameFromContentDisposition, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, parseDateOnlyLocal, primitiveArrayTypes, pushAction, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
5242
|
+
export { Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, FooterComponent, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, capitalizeFirstChar, deleteAction, exportListToExcel, firstCharToUpper, getFileNameFromContentDisposition, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, parseDateOnlyLocal, primitiveArrayTypes, pushAction, saveResponseAsFile, selectedTab, singleOrDefault, splitPascalCase, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
5213
5243
|
//# sourceMappingURL=spiderly.mjs.map
|