@tinya-code/gas-admin-ui 0.1.0-alpha.4 → 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 +568 -64
- 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,26 +1,59 @@
|
|
|
1
1
|
# @tinya-code/gas-admin-ui
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
| Requirement | Version / detail |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Angular | `22.2` (standalone, signals) |
|
|
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 |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick start from scratch
|
|
18
|
+
|
|
19
|
+
### 1. Install the package
|
|
6
20
|
|
|
7
21
|
```bash
|
|
8
22
|
npm i @tinya-code/gas-admin-ui
|
|
9
23
|
```
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
An Angular 22.2 project already has `@angular/*` — verify the remaining peers (`@lucide/angular`, `chart.js`).
|
|
26
|
+
|
|
27
|
+
### 2. Declare the Tailwind source
|
|
13
28
|
|
|
14
|
-
|
|
29
|
+
In `src/styles.css`:
|
|
15
30
|
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
```css
|
|
32
|
+
/* Required: lets Tailwind scan the engine's templates for utility classes */
|
|
33
|
+
@source '../node_modules/@tinya-code/gas-admin-ui/fesm2022';
|
|
34
|
+
|
|
35
|
+
@import 'tailwindcss';
|
|
36
|
+
@import './styles/theme.css'; /* your design tokens */
|
|
37
|
+
@import './styles/base.css'; /* your base resets */
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Without this the app compiles and tests pass, but the engine UI renders with no styles.
|
|
41
|
+
|
|
42
|
+
### 3. Wire the engine — `app.config.ts`
|
|
18
43
|
|
|
19
44
|
```ts
|
|
45
|
+
// src/app/app.config.ts
|
|
46
|
+
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
47
|
+
import { provideRouter } from '@angular/router';
|
|
20
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';
|
|
21
53
|
|
|
22
54
|
export const appConfig: ApplicationConfig = {
|
|
23
55
|
providers: [
|
|
56
|
+
provideBrowserGlobalErrorListeners(),
|
|
24
57
|
provideRouter(routes),
|
|
25
58
|
provideGasAdmin({ schemas, auth: authSchema }),
|
|
26
59
|
{ provide: API_URL, useValue: environment.apiUrl },
|
|
@@ -28,97 +61,568 @@ export const appConfig: ApplicationConfig = {
|
|
|
28
61
|
};
|
|
29
62
|
```
|
|
30
63
|
|
|
31
|
-
|
|
64
|
+
```ts
|
|
65
|
+
// src/environments/environment.ts
|
|
66
|
+
export const environment = {
|
|
67
|
+
production: false,
|
|
68
|
+
apiUrl: 'http://localhost:3000/api/v1',
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. Declare routes — `app.routes.ts`
|
|
32
73
|
|
|
33
74
|
```ts
|
|
75
|
+
// src/app/app.routes.ts
|
|
76
|
+
import { Routes } from '@angular/router';
|
|
34
77
|
import { authGuard, shellRoutes } from '@tinya-code/gas-admin-ui';
|
|
78
|
+
import { schemas } from './schemas/registry';
|
|
79
|
+
|
|
80
|
+
const firstResource = schemas[0]?.id || 'dashboard';
|
|
35
81
|
|
|
36
82
|
export const routes: Routes = [
|
|
37
83
|
{ path: '', pathMatch: 'full', redirectTo: firstResource },
|
|
38
|
-
{
|
|
39
|
-
|
|
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
|
+
},
|
|
40
93
|
{ path: '**', redirectTo: firstResource },
|
|
41
94
|
];
|
|
42
95
|
```
|
|
43
96
|
|
|
44
|
-
|
|
45
|
-
|
|
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`
|
|
117
|
+
|
|
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
|
+
```
|
|
139
|
+
|
|
140
|
+
The array order determines both menu order and the default redirect route.
|
|
141
|
+
|
|
142
|
+
### 7. Start the app
|
|
46
143
|
|
|
47
144
|
```bash
|
|
48
|
-
|
|
145
|
+
npm start
|
|
49
146
|
```
|
|
50
147
|
|
|
51
|
-
|
|
52
|
-
(campos, validators, enums y endpoint derivados de la proyección back), más
|
|
53
|
-
`registry.ts` (el array ordenado que alimenta menú y rutas),
|
|
54
|
-
`auth.schema.ts` (las claves exactas del `input` de `/auth/login`) y
|
|
55
|
-
`seed-catalog.ts` (siembra el catálogo para los specs). Los `label` salen
|
|
56
|
-
de las keys: reemplazá los placeholders por los del dominio y sumá las
|
|
57
|
-
vistas/filtros que quieras (el recorrido de capacidades está en la
|
|
58
|
-
cabecera de `registry.ts`). Estructura equivalente a mano si preferís
|
|
59
|
-
escribirlo. La validación de que todo siga coincidiendo con el back la hace
|
|
60
|
-
`gas-admin validate --with-front` (`contract-check` de
|
|
61
|
-
`@tinya-code/gas-admin`).
|
|
148
|
+
The shell starts at `/login` and, once authenticated, shows the sidebar menu and views derived from your schemas.
|
|
62
149
|
|
|
63
|
-
|
|
150
|
+
---
|
|
64
151
|
|
|
65
|
-
|
|
152
|
+
## Schema examples (real implementations)
|
|
66
153
|
|
|
67
|
-
|
|
68
|
-
`node_modules` y no lo escanea solo. Agregar en `styles.css`:
|
|
154
|
+
The examples below are taken from a production consumer of this library.
|
|
69
155
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
npm i firebase
|
|
72
442
|
```
|
|
73
443
|
|
|
74
|
-
|
|
75
|
-
tests en verde).
|
|
444
|
+
### 2. Declare providers in `AuthSchema`
|
|
76
445
|
|
|
77
|
-
|
|
446
|
+
```ts
|
|
447
|
+
// src/app/schemas/auth.schema.ts
|
|
448
|
+
import type { AuthSchema } from '@tinya-code/gas-admin-ui';
|
|
78
449
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
};
|
|
86
460
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
};
|
|
92
469
|
|
|
93
|
-
|
|
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
|
+
};
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 3. Implement the adapter and wire it in `app.config.ts`
|
|
94
484
|
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
97
505
|
|
|
98
|
-
|
|
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
|
+
}
|
|
519
|
+
|
|
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
|
+
```
|
|
529
|
+
|
|
530
|
+
### 4. Backend endpoint — `POST /auth/firebase`
|
|
531
|
+
|
|
532
|
+
The library sends the Firebase ID token using the **same envelope transport** as every other request. Your backend must:
|
|
533
|
+
|
|
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`.
|
|
537
|
+
|
|
538
|
+
```json
|
|
539
|
+
// Request body (Content-Type: text/plain)
|
|
540
|
+
{ "token": null, "method": "POST", "path": "/auth/firebase", "payload": { "idToken": "..." } }
|
|
541
|
+
|
|
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.
|
|
555
|
+
|
|
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`)
|
|
563
|
+
|
|
564
|
+
```text
|
|
565
|
+
src/app/schemas/
|
|
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)
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
`scaffold` fails with exit 1 if the target folder already contains `*.ts` files — use `--force` to regenerate.
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Public API
|
|
583
|
+
|
|
584
|
+
Only what `public-api.ts` exports is stable contract; everything else is private and may change between alphas.
|
|
585
|
+
|
|
586
|
+
| Group | Exports |
|
|
99
587
|
| --- | --- |
|
|
100
|
-
| Wiring | `provideGasAdmin`, `GAS_SCHEMAS`, `setSchemaCatalog`, `getSchema`, `allSchemas`, `getAuth`, `
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
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` |
|
|
106
594
|
| Forms | `buildFormSchema`, `seedModel`, `buildPayload` |
|
|
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
|
|
606
|
+
|
|
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).
|
|
611
|
+
|
|
612
|
+
---
|
|
107
613
|
|
|
108
|
-
##
|
|
614
|
+
## Development (this repo)
|
|
109
615
|
|
|
110
616
|
```bash
|
|
111
|
-
ng build gas-admin-ui # ng-packagr → dist/gas-admin-ui
|
|
112
|
-
ng test gas-admin-ui # vitest:
|
|
617
|
+
ng build gas-admin-ui # ng-packagr → dist/gas-admin-ui
|
|
618
|
+
ng test gas-admin-ui # vitest: engine tests
|
|
113
619
|
```
|
|
114
620
|
|
|
115
|
-
|
|
621
|
+
Publish (npm requires explicit `--tag` for pre-releases):
|
|
116
622
|
|
|
117
623
|
```bash
|
|
118
624
|
npm publish ./dist/gas-admin-ui --tag alpha
|
|
119
625
|
```
|
|
120
626
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
consumidor importando desde este paquete — ver los 16 specs de
|
|
124
|
-
integración de `admin-schema`.
|
|
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.
|