@tinya-code/gas-admin-ui 0.1.0-alpha.5 → 0.2.0-alpha.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 +538 -117
- package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs → tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs} +4 -4
- package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map → tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs → tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs → tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs} +2 -3
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map → tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs → tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs → tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map → tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs → tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs} +9 -10
- package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map → tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs → tinya-code-gas-admin-ui-image-BlDRZe-F.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map → tinya-code-gas-admin-ui-image-BlDRZe-F.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs → tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs} +4 -4
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map → tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs → tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map → tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs → tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map → tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs → tinya-code-gas-admin-ui-relation-BSqbQB28.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map → tinya-code-gas-admin-ui-relation-BSqbQB28.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs → tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs} +10 -10
- package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map → tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs → tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs} +161 -7
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs → tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map → tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs → tinya-code-gas-admin-ui-select-BUbb36-T.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map → tinya-code-gas-admin-ui-select-BUbb36-T.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs → tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map → tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs.map} +1 -1
- package/fesm2022/tinya-code-gas-admin-ui.mjs +65 -11
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
- package/package.json +8 -2
- package/types/tinya-code-gas-admin-ui.d.ts +69 -3
- package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +0 -131
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,61 +1,59 @@
|
|
|
1
1
|
# @tinya-code/gas-admin-ui
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
dominio (`schemas/*.schema.ts`) y consume el motor compilado — mismo contrato
|
|
5
|
-
que `@tinya-code/gas-admin` en el back, pero para Angular. El paquete dibuja
|
|
6
|
-
vistas, campos, acciones, shell y rutas; vos no tocás componentes a mano.
|
|
3
|
+
Angular rendering engine for the `@tinya-code` ecosystem. You declare your domain in typed schema files — the engine renders views, forms, lists, actions, shell, and routes. You never write components by hand.
|
|
7
4
|
|
|
8
|
-
##
|
|
5
|
+
## Requirements
|
|
9
6
|
|
|
10
|
-
|
|
|
7
|
+
| Requirement | Version / detail |
|
|
11
8
|
| --- | --- |
|
|
12
9
|
| Angular | `22.2` (standalone, signals) |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
| CSS | Tailwind v4 —
|
|
16
|
-
|
|
|
10
|
+
| Angular peers | `@angular/{cdk,common,core,forms,platform-browser,router} ^22.2` |
|
|
11
|
+
| Icons / charts | `@lucide/angular ^1.51`, `chart.js ^4.5` |
|
|
12
|
+
| CSS | Tailwind v4 — with the engine's `@source` ([step 2](#2-declare-the-tailwind-source)) |
|
|
13
|
+
| Backend | [`@tinya-code/gas-admin`](https://www.npmjs.com/package/@tinya-code/gas-admin) — its CLI generates and validates front-end schemas |
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
---
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
## Quick start from scratch
|
|
18
|
+
|
|
19
|
+
### 1. Install the package
|
|
21
20
|
|
|
22
21
|
```bash
|
|
23
22
|
npm i @tinya-code/gas-admin-ui
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
demás peers (`@lucide/angular`, `chart.js` y los `@angular/*` que falten).
|
|
25
|
+
An Angular 22.2 project already has `@angular/*` — verify the remaining peers (`@lucide/angular`, `chart.js`).
|
|
28
26
|
|
|
29
|
-
### 2.
|
|
27
|
+
### 2. Declare the Tailwind source
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
In `src/styles.css`:
|
|
32
30
|
|
|
33
31
|
```css
|
|
32
|
+
/* Required: lets Tailwind scan the engine's templates for utility classes */
|
|
34
33
|
@source '../node_modules/@tinya-code/gas-admin-ui/fesm2022';
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Sin esto la app compila pero la UI del motor sale sin clases (visual roto,
|
|
38
|
-
tests en verde).
|
|
39
|
-
|
|
40
|
-
### 3. Generá los schemas desde el back
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx gas-admin scaffold
|
|
35
|
+
@import 'tailwindcss';
|
|
36
|
+
@import './styles/theme.css'; /* your design tokens */
|
|
37
|
+
@import './styles/base.css'; /* your base resets */
|
|
47
38
|
```
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
Si la carpeta ya tiene `*.ts` falla con exit 1; `--force` regenera.
|
|
40
|
+
Without this the app compiles and tests pass, but the engine UI renders with no styles.
|
|
51
41
|
|
|
52
|
-
###
|
|
42
|
+
### 3. Wire the engine — `app.config.ts`
|
|
53
43
|
|
|
54
44
|
```ts
|
|
45
|
+
// src/app/app.config.ts
|
|
46
|
+
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
47
|
+
import { provideRouter } from '@angular/router';
|
|
55
48
|
import { API_URL, provideGasAdmin } from '@tinya-code/gas-admin-ui';
|
|
49
|
+
import { routes } from './app.routes';
|
|
50
|
+
import { schemas } from './schemas/registry';
|
|
51
|
+
import { authSchema } from './schemas/auth.schema';
|
|
52
|
+
import { environment } from '../environments/environment';
|
|
56
53
|
|
|
57
54
|
export const appConfig: ApplicationConfig = {
|
|
58
55
|
providers: [
|
|
56
|
+
provideBrowserGlobalErrorListeners(),
|
|
59
57
|
provideRouter(routes),
|
|
60
58
|
provideGasAdmin({ schemas, auth: authSchema }),
|
|
61
59
|
{ provide: API_URL, useValue: environment.apiUrl },
|
|
@@ -63,145 +61,568 @@ export const appConfig: ApplicationConfig = {
|
|
|
63
61
|
};
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
```ts
|
|
65
|
+
// src/environments/environment.ts
|
|
66
|
+
export const environment = {
|
|
67
|
+
production: false,
|
|
68
|
+
apiUrl: 'http://localhost:3000/api/v1',
|
|
69
|
+
};
|
|
70
|
+
```
|
|
69
71
|
|
|
70
|
-
###
|
|
72
|
+
### 4. Declare routes — `app.routes.ts`
|
|
71
73
|
|
|
72
74
|
```ts
|
|
75
|
+
// src/app/app.routes.ts
|
|
76
|
+
import { Routes } from '@angular/router';
|
|
73
77
|
import { authGuard, shellRoutes } from '@tinya-code/gas-admin-ui';
|
|
78
|
+
import { schemas } from './schemas/registry';
|
|
79
|
+
|
|
80
|
+
const firstResource = schemas[0]?.id || 'dashboard';
|
|
74
81
|
|
|
75
82
|
export const routes: Routes = [
|
|
76
83
|
{ path: '', pathMatch: 'full', redirectTo: firstResource },
|
|
77
|
-
{
|
|
78
|
-
|
|
84
|
+
{
|
|
85
|
+
path: 'login',
|
|
86
|
+
loadComponent: () => import('@tinya-code/gas-admin-ui').then((m) => m.Login),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
path: '',
|
|
90
|
+
canActivate: [authGuard],
|
|
91
|
+
loadChildren: () => shellRoutes,
|
|
92
|
+
},
|
|
79
93
|
{ path: '**', redirectTo: firstResource },
|
|
80
94
|
];
|
|
81
95
|
```
|
|
82
96
|
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
`login` lives outside the shell on purpose — if it were inside, `authGuard` would block it.
|
|
98
|
+
|
|
99
|
+
### 5. Declare auth — `auth.schema.ts`
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// src/app/schemas/auth.schema.ts
|
|
103
|
+
import type { AuthSchema } from '@tinya-code/gas-admin-ui';
|
|
104
|
+
|
|
105
|
+
export const authSchema: AuthSchema = {
|
|
106
|
+
providers: ['password'],
|
|
107
|
+
loginFields: [
|
|
108
|
+
{ key: 'email', type: 'email', required: true },
|
|
109
|
+
{ key: 'password', type: 'password', required: true },
|
|
110
|
+
],
|
|
111
|
+
redirectAfterLogin: '/dashboard',
|
|
112
|
+
rememberMe: false,
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 6. Declare your schemas — `registry.ts`
|
|
87
117
|
|
|
88
|
-
|
|
118
|
+
```ts
|
|
119
|
+
// src/app/schemas/registry.ts
|
|
120
|
+
import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
|
|
121
|
+
import { dashboardSchema } from './dashboard.schema';
|
|
122
|
+
import { businessSchema } from './business.schema';
|
|
123
|
+
import { clientsSchema } from './clients.schema';
|
|
124
|
+
import { ranksSchema } from './ranks.schema';
|
|
125
|
+
import { rewardsSchema } from './rewards.schema';
|
|
126
|
+
import { couponsSchema } from './coupons.schema';
|
|
127
|
+
import { feedbackSchema } from './feedback.schema';
|
|
128
|
+
|
|
129
|
+
export const schemas: ResourceSchema[] = [
|
|
130
|
+
dashboardSchema,
|
|
131
|
+
businessSchema,
|
|
132
|
+
clientsSchema,
|
|
133
|
+
ranksSchema,
|
|
134
|
+
rewardsSchema,
|
|
135
|
+
couponsSchema,
|
|
136
|
+
feedbackSchema,
|
|
137
|
+
];
|
|
138
|
+
```
|
|
89
139
|
|
|
90
|
-
|
|
91
|
-
dominio y sumá las vistas/filtros que quieras (el recorrido de capacidades
|
|
92
|
-
está en la cabecera de `registry.ts`). También podés escribir toda la
|
|
93
|
-
estructura a mano si preferís no usar el scaffold.
|
|
140
|
+
The array order determines both menu order and the default redirect route.
|
|
94
141
|
|
|
95
|
-
### 7.
|
|
142
|
+
### 7. Start the app
|
|
96
143
|
|
|
97
144
|
```bash
|
|
98
145
|
npm start
|
|
99
146
|
```
|
|
100
147
|
|
|
101
|
-
|
|
102
|
-
las vistas derivadas de tus schemas.
|
|
148
|
+
The shell starts at `/login` and, once authenticated, shows the sidebar menu and views derived from your schemas.
|
|
103
149
|
|
|
104
|
-
|
|
150
|
+
---
|
|
105
151
|
|
|
106
|
-
|
|
152
|
+
## Schema examples (real implementations)
|
|
153
|
+
|
|
154
|
+
The examples below are taken from a production consumer of this library.
|
|
155
|
+
|
|
156
|
+
### Dashboard — `kind: 'dashboard'`
|
|
157
|
+
|
|
158
|
+
A read-only panel with metric cards. No list, no form.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// src/app/schemas/dashboard.schema.ts
|
|
162
|
+
import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
|
|
163
|
+
|
|
164
|
+
export const dashboardSchema: ResourceSchema = {
|
|
165
|
+
id: 'dashboard',
|
|
166
|
+
label: 'Panel',
|
|
167
|
+
labelPlural: 'Dashboard',
|
|
168
|
+
kind: 'dashboard',
|
|
169
|
+
uiViews: {
|
|
170
|
+
overview: { type: 'dashboard' },
|
|
171
|
+
},
|
|
172
|
+
endpoint: {
|
|
173
|
+
get: '/admin/dashboard',
|
|
174
|
+
},
|
|
175
|
+
keyField: 'id',
|
|
176
|
+
titleField: 'label',
|
|
177
|
+
listColumns: [],
|
|
178
|
+
fields: [],
|
|
179
|
+
widgets: [
|
|
180
|
+
{ type: 'metric-card', key: 'totalClients', label: 'Total Clients', hint: 'Registered memberships', width: 4 },
|
|
181
|
+
{ type: 'metric-card', key: 'activeClients30d', label: 'Active (30d)', hint: 'Visited in last 30 days', width: 4 },
|
|
182
|
+
{ type: 'metric-card', key: 'visitsThisMonth', label: 'Visits This Month', hint: 'Current month', width: 4 },
|
|
183
|
+
{ type: 'metric-card', key: 'averageRating', label: 'Average Rating', hint: 'Historical satisfaction', width: 4 },
|
|
184
|
+
{ type: 'metric-card', key: 'issuedCoupons', label: 'Issued Coupons', hint: 'All time', width: 4 },
|
|
185
|
+
{ type: 'metric-card', key: 'usedCoupons', label: 'Redeemed Coupons', hint: 'Effective rewards', width: 4 },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Singleton — `kind: 'singleton'` with sections layout
|
|
193
|
+
|
|
194
|
+
One record, no list. The form is divided into labelled sections.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// src/app/schemas/business.schema.ts
|
|
198
|
+
import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
|
|
199
|
+
|
|
200
|
+
export const businessSchema: ResourceSchema = {
|
|
201
|
+
id: 'business',
|
|
202
|
+
label: 'My Business',
|
|
203
|
+
labelPlural: 'My Business',
|
|
204
|
+
kind: 'singleton',
|
|
205
|
+
uiViews: {
|
|
206
|
+
settings: { type: 'form' },
|
|
207
|
+
},
|
|
208
|
+
endpoint: {
|
|
209
|
+
get: '/admin/businesses/current',
|
|
210
|
+
update: '/admin/businesses/current',
|
|
211
|
+
},
|
|
212
|
+
keyField: 'key',
|
|
213
|
+
titleField: 'name',
|
|
214
|
+
listColumns: [],
|
|
215
|
+
layout: {
|
|
216
|
+
mode: 'sections',
|
|
217
|
+
sections: [
|
|
218
|
+
{ id: 'general', label: 'General' },
|
|
219
|
+
{ id: 'branding', label: 'Design & Card' },
|
|
220
|
+
{ id: 'social', label: 'Social & Links' },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
fields: [
|
|
224
|
+
{ key: 'name', type: 'text', label: 'Business Name', required: true, validators: { minLength: 2, maxLength: 120 }, section: 'general', width: 6 },
|
|
225
|
+
{ key: 'slug', type: 'slug', from: 'name', label: 'Slug', required: true, lockAfterCreate: true, section: 'general', width: 6 },
|
|
226
|
+
{ key: 'timezone', type: 'text', label: 'Timezone', required: true, help: 'e.g. America/Lima', section: 'general', width: 6 },
|
|
227
|
+
{
|
|
228
|
+
key: 'visitResetPolicy',
|
|
229
|
+
type: 'select',
|
|
230
|
+
label: 'Visit Reset Policy',
|
|
231
|
+
section: 'general',
|
|
232
|
+
width: 6,
|
|
233
|
+
options: [
|
|
234
|
+
{ value: 'none', label: 'None' },
|
|
235
|
+
{ value: 'monthly', label: 'Monthly' },
|
|
236
|
+
{ value: 'yearly', label: 'Yearly' },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
{ key: 'colorPrimary', type: 'text', label: 'Primary Color (HEX)', placeholder: '#4A2C11', section: 'branding', width: 6 },
|
|
240
|
+
{ key: 'colorSecondary', type: 'text', label: 'Secondary Color (HEX)', placeholder: '#D4A373', section: 'branding', width: 6 },
|
|
241
|
+
{ key: 'logoUrl', type: 'image', label: 'Logo', readonly: true, section: 'branding', width: 12 },
|
|
242
|
+
{ key: 'googleReviewUrl', type: 'url', label: 'Google Reviews URL', section: 'social', width: 6 },
|
|
243
|
+
{ key: 'instagramUrl', type: 'url', label: 'Instagram', section: 'social', width: 6 },
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### Collection with full CRUD — `kind: 'collection'`
|
|
251
|
+
|
|
252
|
+
List + create + edit. Includes search, filters, and field validators.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// src/app/schemas/ranks.schema.ts
|
|
256
|
+
import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
|
|
257
|
+
|
|
258
|
+
export const ranksSchema: ResourceSchema = {
|
|
259
|
+
id: 'ranks',
|
|
260
|
+
label: 'Rank',
|
|
261
|
+
labelPlural: 'Ranks',
|
|
262
|
+
kind: 'collection',
|
|
263
|
+
uiViews: {
|
|
264
|
+
list: { type: 'list' },
|
|
265
|
+
create: { type: 'form', path: 'new' },
|
|
266
|
+
edit: { type: 'form', path: ':key/edit' },
|
|
267
|
+
},
|
|
268
|
+
endpoint: {
|
|
269
|
+
list: '/admin/businesses/current/ranks',
|
|
270
|
+
get: '/admin/businesses/current/ranks/{key}',
|
|
271
|
+
create: '/admin/businesses/current/ranks',
|
|
272
|
+
update: '/admin/businesses/current/ranks/{key}',
|
|
273
|
+
remove: '/admin/businesses/current/ranks/{key}',
|
|
274
|
+
},
|
|
275
|
+
keyField: 'id',
|
|
276
|
+
titleField: 'name',
|
|
277
|
+
search: ['name'],
|
|
278
|
+
filters: [
|
|
279
|
+
{ key: 'includeInactive', label: 'Include Inactive', type: 'boolean' },
|
|
280
|
+
],
|
|
281
|
+
listColumns: [
|
|
282
|
+
{ key: 'level', label: 'Level' },
|
|
283
|
+
{ key: 'name', label: 'Name' },
|
|
284
|
+
{ key: 'minVisits', label: 'Min Visits' },
|
|
285
|
+
{ key: 'benefitType', label: 'Benefit Type' },
|
|
286
|
+
{ key: 'benefitValue',label: 'Value' },
|
|
287
|
+
{ key: 'active', label: 'Active', format: 'boolean' },
|
|
288
|
+
],
|
|
289
|
+
fields: [
|
|
290
|
+
{ key: 'name', type: 'text', label: 'Rank Name', required: true, validators: { minLength: 1, maxLength: 80 } },
|
|
291
|
+
{ key: 'level', type: 'number', label: 'Level', required: true, min: 1, help: 'Must be unique among active ranks' },
|
|
292
|
+
{ key: 'minVisits',type: 'number', label: 'Minimum Visits', required: true, min: 0 },
|
|
293
|
+
{
|
|
294
|
+
key: 'benefitType',
|
|
295
|
+
type: 'select',
|
|
296
|
+
label: 'Benefit Type',
|
|
297
|
+
required: true,
|
|
298
|
+
options: [
|
|
299
|
+
{ value: 'none', label: 'None' },
|
|
300
|
+
{ value: 'percentage_discount', label: 'Percentage Discount' },
|
|
301
|
+
{ value: 'fixed_discount', label: 'Fixed Discount' },
|
|
302
|
+
{ value: 'free_item', label: 'Free Item' },
|
|
303
|
+
{ value: 'priority', label: 'Priority Service' },
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
{ key: 'benefitValue', type: 'number', label: 'Benefit Value' },
|
|
307
|
+
{ key: 'benefitDescription', type: 'textarea', label: 'Benefit Description', validators: { maxLength: 200 } },
|
|
308
|
+
{ key: 'color', type: 'text', label: 'Color (HEX)', placeholder: '#CD7F32' },
|
|
309
|
+
{ key: 'iconUrl', type: 'image', label: 'Icon', readonly: true },
|
|
310
|
+
{ key: 'active', type: 'boolean', label: 'Active', readonlyOn: 'create' },
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Read-only collection with detail view
|
|
318
|
+
|
|
319
|
+
List + detail only — no create, no edit form.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
// src/app/schemas/clients.schema.ts
|
|
323
|
+
import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
|
|
324
|
+
|
|
325
|
+
export const clientsSchema: ResourceSchema = {
|
|
326
|
+
id: 'clients',
|
|
327
|
+
label: 'Client',
|
|
328
|
+
labelPlural: 'Clients',
|
|
329
|
+
kind: 'collection',
|
|
330
|
+
uiViews: {
|
|
331
|
+
list: { type: 'list' },
|
|
332
|
+
detail: { type: 'detail', path: ':key' },
|
|
333
|
+
edit: { type: 'form', path: ':key/edit' },
|
|
334
|
+
},
|
|
335
|
+
endpoint: {
|
|
336
|
+
list: '/admin/businesses/current/clients',
|
|
337
|
+
get: '/admin/businesses/current/clients/{key}',
|
|
338
|
+
update: '/admin/businesses/current/clients/{key}/status',
|
|
339
|
+
// no create / remove → those actions won't appear in the UI
|
|
340
|
+
},
|
|
341
|
+
keyField: 'membershipId',
|
|
342
|
+
titleField: 'name',
|
|
343
|
+
search: ['name', 'phone'],
|
|
344
|
+
filters: [
|
|
345
|
+
{ key: 'active', label: 'Active', type: 'boolean' },
|
|
346
|
+
],
|
|
347
|
+
listColumns: [
|
|
348
|
+
{ key: 'name', label: 'Name' },
|
|
349
|
+
{ key: 'phone', label: 'Phone' },
|
|
350
|
+
{ key: 'email', label: 'Email' },
|
|
351
|
+
{ key: 'totalVisits', label: 'Total Visits' },
|
|
352
|
+
{ key: 'lastVisitAt', label: 'Last Visit' },
|
|
353
|
+
{ key: 'active', label: 'Active', format: 'boolean' },
|
|
354
|
+
{ key: 'registeredAt',label: 'Registered' },
|
|
355
|
+
],
|
|
356
|
+
detail: {
|
|
357
|
+
enabled: true,
|
|
358
|
+
headerFields: ['name', 'phone', 'active'],
|
|
359
|
+
},
|
|
360
|
+
fields: [
|
|
361
|
+
{ key: 'membershipId', type: 'readonly-text', label: 'Membership ID', readonly: true },
|
|
362
|
+
{ key: 'name', type: 'text', label: 'Name', readonly: true },
|
|
363
|
+
{ key: 'phone', type: 'phone', label: 'Phone', readonly: true },
|
|
364
|
+
{ key: 'email', type: 'email', label: 'Email', readonly: true },
|
|
365
|
+
{ key: 'active', type: 'boolean', label: 'Active Membership' },
|
|
366
|
+
{ key: 'totalVisits', type: 'number', label: 'Total Visits', readonly: true },
|
|
367
|
+
{ key: 'currentPeriodVisits', type: 'number', label: 'Period Visits', readonly: true },
|
|
368
|
+
{ key: 'registeredAt', type: 'date', label: 'Registered At', readonly: true },
|
|
369
|
+
{ key: 'lastVisitAt', type: 'date', label: 'Last Visit', readonly: true },
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### Read-only collection with select filter
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
// src/app/schemas/coupons.schema.ts
|
|
380
|
+
import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
|
|
381
|
+
|
|
382
|
+
export const couponsSchema: ResourceSchema = {
|
|
383
|
+
id: 'coupons',
|
|
384
|
+
label: 'Coupon',
|
|
385
|
+
labelPlural: 'Coupons',
|
|
386
|
+
kind: 'collection',
|
|
387
|
+
uiViews: {
|
|
388
|
+
list: { type: 'list' },
|
|
389
|
+
detail: { type: 'detail', path: ':key' },
|
|
390
|
+
},
|
|
391
|
+
endpoint: {
|
|
392
|
+
list: '/admin/businesses/current/coupons',
|
|
393
|
+
get: '/admin/businesses/current/coupons/{key}',
|
|
394
|
+
},
|
|
395
|
+
keyField: 'id',
|
|
396
|
+
titleField: 'code',
|
|
397
|
+
search: ['code'],
|
|
398
|
+
filters: [
|
|
399
|
+
{
|
|
400
|
+
key: 'status',
|
|
401
|
+
label: 'Status',
|
|
402
|
+
type: 'select',
|
|
403
|
+
options: [
|
|
404
|
+
{ value: 'issued', label: 'Issued' },
|
|
405
|
+
{ value: 'used', label: 'Redeemed' },
|
|
406
|
+
{ value: 'expired', label: 'Expired' },
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
listColumns: [
|
|
411
|
+
{ key: 'code', label: 'Code' },
|
|
412
|
+
{ key: 'status', label: 'Status' },
|
|
413
|
+
{ key: 'issuedAt', label: 'Issued' },
|
|
414
|
+
{ key: 'expiresAt',label: 'Expires' },
|
|
415
|
+
{ key: 'usedAt', label: 'Redeemed' },
|
|
416
|
+
],
|
|
417
|
+
detail: {
|
|
418
|
+
enabled: true,
|
|
419
|
+
headerFields: ['code', 'status', 'issuedAt'],
|
|
420
|
+
},
|
|
421
|
+
fields: [
|
|
422
|
+
{ key: 'id', type: 'readonly-text', label: 'ID', readonly: true },
|
|
423
|
+
{ key: 'code', type: 'text', label: 'Code', readonly: true },
|
|
424
|
+
{ key: 'status', type: 'text', label: 'Status', readonly: true },
|
|
425
|
+
{ key: 'issuedAt', type: 'date', label: 'Issued At', readonly: true },
|
|
426
|
+
{ key: 'expiresAt', type: 'date', label: 'Expires At', readonly: true },
|
|
427
|
+
{ key: 'usedAt', type: 'date', label: 'Redeemed At',readonly: true },
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Firebase Auth
|
|
435
|
+
|
|
436
|
+
The package supports federated authentication with Google and Firebase Email/Password via the decoupled `FirebaseAuthAdapter`. Firebase is declared as an optional `peerDependency` — the library does not bundle it.
|
|
437
|
+
|
|
438
|
+
### 1. Install Firebase in the consumer app
|
|
107
439
|
|
|
108
440
|
```bash
|
|
109
|
-
|
|
441
|
+
npm i firebase
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### 2. Declare providers in `AuthSchema`
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
// src/app/schemas/auth.schema.ts
|
|
448
|
+
import type { AuthSchema } from '@tinya-code/gas-admin-ui';
|
|
449
|
+
|
|
450
|
+
// Option A — password only (default, no Firebase required)
|
|
451
|
+
export const authSchema: AuthSchema = {
|
|
452
|
+
providers: ['password'],
|
|
453
|
+
loginFields: [
|
|
454
|
+
{ key: 'email', type: 'email', required: true },
|
|
455
|
+
{ key: 'password', type: 'password', required: true },
|
|
456
|
+
],
|
|
457
|
+
redirectAfterLogin: '/dashboard',
|
|
458
|
+
rememberMe: false,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Option B — Google only
|
|
462
|
+
export const authSchema: AuthSchema = {
|
|
463
|
+
providers: ['google'],
|
|
464
|
+
loginFields: [],
|
|
465
|
+
redirectAfterLogin: '/dashboard',
|
|
466
|
+
rememberMe: false,
|
|
467
|
+
firebase: { exchangeEndpoint: '/auth/firebase' },
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// Option C — Google + Password (both)
|
|
471
|
+
export const authSchema: AuthSchema = {
|
|
472
|
+
providers: ['google', 'password'],
|
|
473
|
+
loginFields: [
|
|
474
|
+
{ key: 'email', type: 'email', required: true },
|
|
475
|
+
{ key: 'password', type: 'password', required: true },
|
|
476
|
+
],
|
|
477
|
+
redirectAfterLogin: '/dashboard',
|
|
478
|
+
rememberMe: false,
|
|
479
|
+
firebase: { exchangeEndpoint: '/auth/firebase' },
|
|
480
|
+
};
|
|
110
481
|
```
|
|
111
482
|
|
|
112
|
-
|
|
113
|
-
front siguen coincidiendo con la proyección del back.
|
|
483
|
+
### 3. Implement the adapter and wire it in `app.config.ts`
|
|
114
484
|
|
|
115
|
-
|
|
485
|
+
```ts
|
|
486
|
+
// src/app/app.config.ts
|
|
487
|
+
import { initializeApp } from 'firebase/app';
|
|
488
|
+
import {
|
|
489
|
+
getAuth,
|
|
490
|
+
GoogleAuthProvider,
|
|
491
|
+
signInWithPopup,
|
|
492
|
+
signInWithEmailAndPassword,
|
|
493
|
+
signOut,
|
|
494
|
+
} from 'firebase/auth';
|
|
495
|
+
import {
|
|
496
|
+
API_URL,
|
|
497
|
+
provideGasAdmin,
|
|
498
|
+
provideFirebaseAdapter,
|
|
499
|
+
} from '@tinya-code/gas-admin-ui';
|
|
500
|
+
import type { FirebaseAuthAdapter } from '@tinya-code/gas-admin-ui';
|
|
501
|
+
import { environment } from '../environments/environment';
|
|
502
|
+
|
|
503
|
+
const firebaseApp = initializeApp(environment.firebase);
|
|
504
|
+
const firebaseAuth = getAuth(firebaseApp);
|
|
505
|
+
|
|
506
|
+
class AppFirebaseAuthAdapter implements FirebaseAuthAdapter {
|
|
507
|
+
async signInWithGoogle(): Promise<string> {
|
|
508
|
+
const cred = await signInWithPopup(firebaseAuth, new GoogleAuthProvider());
|
|
509
|
+
return cred.user.getIdToken();
|
|
510
|
+
}
|
|
511
|
+
async signInWithEmailAndPassword(email: string, password: string): Promise<string> {
|
|
512
|
+
const cred = await signInWithEmailAndPassword(firebaseAuth, email, password);
|
|
513
|
+
return cred.user.getIdToken();
|
|
514
|
+
}
|
|
515
|
+
async signOut(): Promise<void> {
|
|
516
|
+
await signOut(firebaseAuth);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
116
519
|
|
|
117
|
-
|
|
520
|
+
export const appConfig: ApplicationConfig = {
|
|
521
|
+
providers: [
|
|
522
|
+
provideRouter(routes),
|
|
523
|
+
provideGasAdmin({ schemas, auth: authSchema }),
|
|
524
|
+
provideFirebaseAdapter(new AppFirebaseAuthAdapter()), // ← add this
|
|
525
|
+
{ provide: API_URL, useValue: environment.apiUrl },
|
|
526
|
+
],
|
|
527
|
+
};
|
|
528
|
+
```
|
|
118
529
|
|
|
119
|
-
|
|
120
|
-
`node_modules` y Tailwind no lo escanea solo. El `@source` del paso 2 es
|
|
121
|
-
obligatorio: sin él la app compila y los tests quedan en verde, pero la UI
|
|
122
|
-
del motor se renderiza sin estilos.
|
|
530
|
+
### 4. Backend endpoint — `POST /auth/firebase`
|
|
123
531
|
|
|
124
|
-
|
|
532
|
+
The library sends the Firebase ID token using the **same envelope transport** as every other request. Your backend must:
|
|
125
533
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
y los tests usan la MISMA copia compilada ⇒ Angular se instancia una sola
|
|
130
|
-
vez (verificado en `admin-schema`: 116/116 sin alias). Para probar un cambio
|
|
131
|
-
del motor, publicá un alpha y bumpeá la dependencia.
|
|
534
|
+
1. Receive `{ idToken: string }` in the payload.
|
|
535
|
+
2. Verify it with the Firebase Admin SDK (`getAuth().verifyIdToken(idToken)`).
|
|
536
|
+
3. Return `{ token, user }` — identical shape to `/auth/login`.
|
|
132
537
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
tests (`paths` en `tsconfig.spec.json` apuntando al source del paquete)
|
|
137
|
-
junto con un clone del repo como directorio hermano en CI.
|
|
538
|
+
```json
|
|
539
|
+
// Request body (Content-Type: text/plain)
|
|
540
|
+
{ "token": null, "method": "POST", "path": "/auth/firebase", "payload": { "idToken": "..." } }
|
|
138
541
|
|
|
139
|
-
|
|
542
|
+
// Response
|
|
543
|
+
{ "token": "<your-app-jwt>", "user": { "id": "...", "email": "...", "name": "...", "role": "..." }, "meta": {} }
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
For a full consumer integration guide including a backend handler example, environment config, and a pre-production checklist, see [`doc/firebase-consumer-example.md`](../../doc/firebase-consumer-example.md).
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Consumer requirements
|
|
551
|
+
|
|
552
|
+
### Tailwind v4 — mandatory `@source`
|
|
553
|
+
|
|
554
|
+
Tailwind generates utilities by scanning **your** source files. The engine lives in `node_modules` and is not scanned by default. The `@source` directive in `styles.css` (step 2) is required — without it the app compiles and tests pass green, but the engine UI renders without any styles.
|
|
140
555
|
|
|
141
|
-
|
|
142
|
-
|
|
556
|
+
### Tests — resolve from `node_modules` (no alias)
|
|
557
|
+
|
|
558
|
+
Consumer specs must resolve `@tinya-code/gas-admin-ui` from `node_modules` (the published, compiled version), without `tsconfig.spec.json` aliases. This guarantees Angular is instantiated exactly once. To test a library change, publish an alpha and bump the dependency.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Generated schema structure (`npx gas-admin scaffold`)
|
|
143
563
|
|
|
144
564
|
```text
|
|
145
565
|
src/app/schemas/
|
|
146
|
-
├── auth.schema.ts #
|
|
147
|
-
├── registry.ts # array
|
|
148
|
-
├── seed-catalog.ts #
|
|
149
|
-
├──
|
|
150
|
-
├──
|
|
151
|
-
├──
|
|
152
|
-
├──
|
|
153
|
-
├──
|
|
154
|
-
|
|
566
|
+
├── auth.schema.ts # auth provider and login fields
|
|
567
|
+
├── registry.ts # ordered array that feeds the menu and routes
|
|
568
|
+
├── seed-catalog.ts # seeds the catalog for specs
|
|
569
|
+
├── dashboard.schema.ts # kind: dashboard
|
|
570
|
+
├── business.schema.ts # kind: singleton
|
|
571
|
+
├── clients.schema.ts # kind: collection (read-only with detail)
|
|
572
|
+
├── ranks.schema.ts # kind: collection (full CRUD)
|
|
573
|
+
├── rewards.schema.ts # kind: collection (full CRUD)
|
|
574
|
+
├── coupons.schema.ts # kind: collection (read-only with filters)
|
|
575
|
+
└── feedback.schema.ts # kind: collection (read-only with search)
|
|
155
576
|
```
|
|
156
577
|
|
|
157
|
-
|
|
158
|
-
derivados de la proyección back.
|
|
578
|
+
`scaffold` fails with exit 1 if the target folder already contains `*.ts` files — use `--force` to regenerate.
|
|
159
579
|
|
|
160
|
-
|
|
580
|
+
---
|
|
161
581
|
|
|
162
|
-
|
|
163
|
-
puede cambiar entre alphas.
|
|
582
|
+
## Public API
|
|
164
583
|
|
|
165
|
-
|
|
584
|
+
Only what `public-api.ts` exports is stable contract; everything else is private and may change between alphas.
|
|
585
|
+
|
|
586
|
+
| Group | Exports |
|
|
166
587
|
| --- | --- |
|
|
167
|
-
| Wiring
|
|
168
|
-
|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
|
|
|
588
|
+
| Wiring & catalog | `provideGasAdmin`, `provideFirebaseAdapter`, `GAS_SCHEMAS`, `setSchemaCatalog`, `getSchema`, `allSchemas`, `getAuth`, type `GasAdminConfig` |
|
|
589
|
+
| Tokens & adapters | `API_URL`, `FIREBASE_AUTH_ADAPTER`, type `FirebaseAuthAdapter` |
|
|
590
|
+
| Routes & guard | `authGuard`, `shellRoutes`, `resourceRoutes` |
|
|
591
|
+
| Pages | `Login`, `Dashboard`, `ListView`, `FormView`, `ResourcePage` |
|
|
592
|
+
| Components | `Sidebar`, `ComposerHost`, `FieldHost`, `ConfirmDialog`, `Toast` |
|
|
593
|
+
| Services | `ApiService`, `AuthService` (+ `SESSION_TOKEN_KEY`, `SESSION_USER_KEY`), `NotificationService`, `DataSourceService`, `UploadService`, `ErrorMapperService`, `apiErrorInterceptor` |
|
|
173
594
|
| Forms | `buildFormSchema`, `seedModel`, `buildPayload` |
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
|
595
|
+
| Schema models | `ResourceSchema`, `AuthSchema`, `AuthProvider`, `FirebaseAuthConfig`, `ResourceAction`, `ApiError` and the full field/widget/view/layout vocabulary |
|
|
596
|
+
| Render registries | `fieldDescriptors`, `columnFormatDescriptors`, `viewDescriptors`, `layoutDescriptors`, `renderKinds` |
|
|
597
|
+
| Custom render | `registerRenderDescriptor`, `unregisterRenderDescriptor`, `provideRenderDescriptors`, `getRenderDescriptor`, `isSupported`, `isRenderSupported`, types `RenderKind`, `ComponentRenderKind` |
|
|
598
|
+
| Composition | `validateComposition`, `MAX_DEPTH`, `SchemaCompositionError` |
|
|
599
|
+
| Custom actions | `registerAction`, `getAction`, `actionsFor`, `actionType`, `actionTypes`, types `ActionDescriptor`, `ActionSurface`, `ActionContext`, `ActionResult` |
|
|
600
|
+
| Validators | type `ValidatorName` |
|
|
601
|
+
| Utilities | `interpolateTemplate` |
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## See also
|
|
181
606
|
|
|
182
|
-
|
|
607
|
+
- **Backend** — domain declaration, `doc`, `validate`:
|
|
608
|
+
[`@tinya-code/gas-admin`](https://github.com/Tinya-Code/gas-admin) → `docs/guia-de-uso.md`.
|
|
609
|
+
- **Firebase consumer guide** — full wiring, backend contract, pre-production checklist:
|
|
610
|
+
[`doc/firebase-consumer-example.md`](../../doc/firebase-consumer-example.md).
|
|
183
611
|
|
|
184
|
-
|
|
185
|
-
guía de `@tinya-code/gas-admin` en
|
|
186
|
-
[`Tinya-Code/gas-admin`](https://github.com/Tinya-Code/gas-admin)
|
|
187
|
-
(`docs/guia-de-uso.md`).
|
|
188
|
-
- **Consumidor de referencia** — `admin-schema`: declara el dominio e integra
|
|
189
|
-
este paquete (116 tests sin alias, specs de integración).
|
|
612
|
+
---
|
|
190
613
|
|
|
191
|
-
##
|
|
614
|
+
## Development (this repo)
|
|
192
615
|
|
|
193
616
|
```bash
|
|
194
|
-
ng build gas-admin-ui # ng-packagr → dist/gas-admin-ui
|
|
195
|
-
ng test gas-admin-ui # vitest: tests
|
|
617
|
+
ng build gas-admin-ui # ng-packagr → dist/gas-admin-ui
|
|
618
|
+
ng test gas-admin-ui # vitest: engine tests
|
|
196
619
|
```
|
|
197
620
|
|
|
198
|
-
|
|
621
|
+
Publish (npm requires explicit `--tag` for pre-releases):
|
|
199
622
|
|
|
200
623
|
```bash
|
|
201
624
|
npm publish ./dist/gas-admin-ui --tag alpha
|
|
202
625
|
```
|
|
203
626
|
|
|
204
|
-
- `dist/`
|
|
205
|
-
-
|
|
206
|
-
acá (la lib no puede importar al consumidor): viven en el repo del
|
|
207
|
-
consumidor importando desde este paquete.
|
|
627
|
+
- `dist/` is gitignored — the build is always local and never committed.
|
|
628
|
+
- Specs that render a consumer's real schemas live in the consumer repo, not here.
|