@praxisui/core 9.0.4-rc.18 → 9.0.4-rc.19

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.
@@ -14021,8 +14021,341 @@ const SURFACE_OPEN_PRESETS = [
14021
14021
  },
14022
14022
  ];
14023
14023
 
14024
+ const RELATED_RESOURCE_OUTLET_I18N_NAMESPACE = 'relatedResourceOutlet';
14025
+ const RELATED_RESOURCE_OUTLET_I18N_CONFIG = {
14026
+ namespaces: {
14027
+ [RELATED_RESOURCE_OUTLET_I18N_NAMESPACE]: {
14028
+ 'pt-BR': {
14029
+ 'state.idle.title': 'Recurso relacionado não selecionado',
14030
+ 'state.idle.description': 'Selecione uma surface relacionada para carregar os dados.',
14031
+ 'state.resolving.title': 'Resolvendo recurso relacionado',
14032
+ 'state.resolving.description': 'Validando metadados e permissões da relação.',
14033
+ 'state.loading.title': 'Carregando recurso relacionado',
14034
+ 'state.loading.description': 'Aguarde enquanto a coleção relacionada é carregada.',
14035
+ 'state.empty.title': 'Nenhuma operação de leitura publicada',
14036
+ 'state.empty.description': 'A relação existe, mas não há operação de lista ou filtro disponível.',
14037
+ 'state.permission-limited.title': 'Acesso limitado',
14038
+ 'state.permission-limited.description': 'O contexto atual não permite abrir este recurso relacionado.',
14039
+ 'state.not-found.title': 'Relação indisponível',
14040
+ 'state.not-found.description': 'Não foi possível resolver a relação ou o identificador do registro pai.',
14041
+ 'state.error.title': 'Falha ao preparar recurso relacionado',
14042
+ 'state.error.description': 'Ocorreu um erro ao preparar a superfície relacionada.',
14043
+ 'emptyState.related.title': 'Sem registros em {label}',
14044
+ 'emptyState.related.description': 'Esta coleção relacionada não possui registros para o contexto selecionado.',
14045
+ 'emptyState.related.descriptionWithAction': 'Use a ação principal para adicionar um registro relacionado quando houver informações para registrar.',
14046
+ 'emptyState.related.action.create': 'Adicionar registro',
14047
+ 'action.open': 'Abrir relacionado',
14048
+ 'status.ready': 'Recurso relacionado pronto',
14049
+ },
14050
+ 'en-US': {
14051
+ 'state.idle.title': 'Related resource not selected',
14052
+ 'state.idle.description': 'Select a related surface to load its data.',
14053
+ 'state.resolving.title': 'Resolving related resource',
14054
+ 'state.resolving.description': 'Validating relation metadata and permissions.',
14055
+ 'state.loading.title': 'Loading related resource',
14056
+ 'state.loading.description': 'Wait while the related collection is loaded.',
14057
+ 'state.empty.title': 'No read operation published',
14058
+ 'state.empty.description': 'The relation exists, but no list or filter operation is available.',
14059
+ 'state.permission-limited.title': 'Limited access',
14060
+ 'state.permission-limited.description': 'The current context cannot open this related resource.',
14061
+ 'state.not-found.title': 'Relation unavailable',
14062
+ 'state.not-found.description': 'The relation or parent record identifier could not be resolved.',
14063
+ 'state.error.title': 'Failed to prepare related resource',
14064
+ 'state.error.description': 'An error occurred while preparing the related surface.',
14065
+ 'emptyState.related.title': 'No records in {label}',
14066
+ 'emptyState.related.description': 'This related collection has no records for the selected context.',
14067
+ 'emptyState.related.descriptionWithAction': 'Use the primary action to add a related record when there is information to capture.',
14068
+ 'emptyState.related.action.create': 'Add record',
14069
+ 'action.open': 'Open related',
14070
+ 'status.ready': 'Related resource ready',
14071
+ },
14072
+ },
14073
+ },
14074
+ };
14075
+
14076
+ class RelatedResourceSurfaceResolverService {
14077
+ i18n = inject(PraxisI18nService, { optional: true });
14078
+ resolve(request) {
14079
+ try {
14080
+ if (!request?.surface) {
14081
+ return { state: 'idle', reason: 'surface-not-provided' };
14082
+ }
14083
+ const surface = request.surface;
14084
+ if (surface.availability?.allowed === false) {
14085
+ return {
14086
+ state: 'permission-limited',
14087
+ reason: surface.availability.reason || 'surface-not-allowed',
14088
+ surface,
14089
+ relatedResource: surface.relatedResource ?? null,
14090
+ };
14091
+ }
14092
+ const relatedResource = surface.relatedResource ?? null;
14093
+ if (!this.isCompleteRelatedResource(relatedResource)) {
14094
+ return {
14095
+ state: 'not-found',
14096
+ reason: 'related-resource-not-published',
14097
+ surface,
14098
+ relatedResource,
14099
+ };
14100
+ }
14101
+ if (!this.hasReadOperation(relatedResource)) {
14102
+ return {
14103
+ state: 'empty',
14104
+ reason: 'related-resource-read-operation-not-published',
14105
+ surface,
14106
+ relatedResource,
14107
+ };
14108
+ }
14109
+ const parentResourceId = this.resolveParentResourceId(request, relatedResource);
14110
+ if (parentResourceId == null || parentResourceId === '') {
14111
+ return {
14112
+ state: 'not-found',
14113
+ reason: 'parent-resource-id-not-resolved',
14114
+ surface,
14115
+ relatedResource,
14116
+ };
14117
+ }
14118
+ const queryContext = this.buildQueryContext(request.queryContext, relatedResource.childParentField, parentResourceId);
14119
+ const payload = this.buildPayload(surface, relatedResource, parentResourceId, queryContext, request);
14120
+ return {
14121
+ state: 'ready',
14122
+ surface,
14123
+ relatedResource,
14124
+ parentResourceId,
14125
+ childResourcePath: relatedResource.childResourcePath,
14126
+ childResourceKey: relatedResource.childResourceKey,
14127
+ queryContext,
14128
+ payload,
14129
+ };
14130
+ }
14131
+ catch (error) {
14132
+ return {
14133
+ state: 'error',
14134
+ reason: error instanceof Error ? error.message : 'related-resource-resolution-failed',
14135
+ surface: request?.surface ?? null,
14136
+ relatedResource: request?.surface?.relatedResource ?? null,
14137
+ };
14138
+ }
14139
+ }
14140
+ state(state, reason) {
14141
+ return { state, reason };
14142
+ }
14143
+ buildPayload(surface, relatedResource, parentResourceId, queryContext, request) {
14144
+ const preset = SURFACE_OPEN_PRESETS.find((candidate) => candidate.id === 'praxis-table');
14145
+ if (!preset) {
14146
+ throw new Error('Missing canonical surface preset "praxis-table".');
14147
+ }
14148
+ const payload = this.clone(preset.payload);
14149
+ payload.presentation = request.presentation ?? payload.presentation;
14150
+ payload.title = request.title || surface.title;
14151
+ payload.subtitle = request.subtitle || surface.description || undefined;
14152
+ payload.icon = request.icon || payload.icon;
14153
+ payload.widget.inputs = {
14154
+ ...(payload.widget.inputs || {}),
14155
+ configPersistenceStrategy: 'volatile',
14156
+ resourcePath: this.normalizeResourcePath(relatedResource.childResourcePath),
14157
+ apiEndpointKey: request.apiEndpointKey ?? null,
14158
+ apiUrlEntry: request.apiUrlEntry ?? null,
14159
+ tableId: request.tableId || this.buildStableTableId(surface, relatedResource, parentResourceId),
14160
+ config: this.buildTableConfig(surface, relatedResource, request),
14161
+ queryContext,
14162
+ enableCustomization: request.enableCustomization === true,
14163
+ ...(this.trim(request.authoringCapability)
14164
+ ? { authoringCapability: this.trim(request.authoringCapability) }
14165
+ : {}),
14166
+ };
14167
+ payload.context = {
14168
+ ...(payload.context || {}),
14169
+ resource: {
14170
+ resourceKey: surface.resourceKey,
14171
+ resourcePath: this.normalizeResourcePath(request.parentResourcePath || ''),
14172
+ resourceId: parentResourceId,
14173
+ },
14174
+ surface,
14175
+ relatedResource,
14176
+ childResource: {
14177
+ resourceKey: relatedResource.childResourceKey,
14178
+ resourcePath: this.normalizeResourcePath(relatedResource.childResourcePath),
14179
+ parentField: relatedResource.childParentField,
14180
+ selectable: relatedResource.selectable,
14181
+ selectionKeyField: relatedResource.selectionKeyField,
14182
+ operations: relatedResource.childOperations,
14183
+ },
14184
+ };
14185
+ return payload;
14186
+ }
14187
+ buildTableConfig(surface, relatedResource, request) {
14188
+ const tableConfig = request.tableConfig;
14189
+ const emptyState = request.emptyState;
14190
+ const hasTableConfig = !!tableConfig && typeof tableConfig === 'object' && !Array.isArray(tableConfig);
14191
+ const hasEmptyState = !!emptyState && typeof emptyState === 'object' && !Array.isArray(emptyState);
14192
+ const base = hasTableConfig ? tableConfig : {};
14193
+ const toolbar = this.objectValue(base['toolbar']);
14194
+ const columnsVisibility = this.objectValue(toolbar['columnsVisibility']);
14195
+ const behavior = this.objectValue(base['behavior']);
14196
+ const currentEmptyState = this.objectValue(behavior['emptyState']);
14197
+ const hasCurrentEmptyState = Object.keys(currentEmptyState).length > 0;
14198
+ const resolvedEmptyState = hasEmptyState
14199
+ ? {
14200
+ ...currentEmptyState,
14201
+ ...emptyState,
14202
+ }
14203
+ : hasCurrentEmptyState
14204
+ ? currentEmptyState
14205
+ : this.buildRelatedEmptyState(surface, relatedResource, request);
14206
+ if (!hasTableConfig && !resolvedEmptyState) {
14207
+ return undefined;
14208
+ }
14209
+ return {
14210
+ ...base,
14211
+ toolbar: {
14212
+ ...toolbar,
14213
+ columnsVisibility: {
14214
+ enabled: false,
14215
+ ...columnsVisibility,
14216
+ },
14217
+ },
14218
+ behavior: {
14219
+ ...behavior,
14220
+ emptyState: resolvedEmptyState,
14221
+ },
14222
+ };
14223
+ }
14224
+ buildRelatedEmptyState(surface, relatedResource, request) {
14225
+ const label = this.trim(request.title)
14226
+ || this.trim(surface.title)
14227
+ || this.humanizeResourceKey(relatedResource.childResourceKey);
14228
+ const canCreate = relatedResource.childOperations.includes('CREATE');
14229
+ const descriptionKey = canCreate
14230
+ ? 'emptyState.related.descriptionWithAction'
14231
+ : 'emptyState.related.description';
14232
+ const actions = canCreate
14233
+ ? [
14234
+ {
14235
+ label: this.t('emptyState.related.action.create', 'Adicionar registro'),
14236
+ action: 'create',
14237
+ icon: 'add',
14238
+ primary: true,
14239
+ },
14240
+ ]
14241
+ : [];
14242
+ return {
14243
+ title: this.t('emptyState.related.title', 'Sem registros em {label}', { label }),
14244
+ message: this.t(descriptionKey, canCreate
14245
+ ? 'Use a ação principal para adicionar um registro relacionado quando houver informações para registrar.'
14246
+ : 'Esta coleção relacionada não possui registros para o contexto selecionado.', { label }),
14247
+ icon: this.trim(request.icon) || 'hub',
14248
+ tone: 'neutral',
14249
+ variant: 'inline',
14250
+ density: 'compact',
14251
+ alignment: 'center',
14252
+ iconContainer: 'soft',
14253
+ actions,
14254
+ };
14255
+ }
14256
+ objectValue(value) {
14257
+ return value && typeof value === 'object' && !Array.isArray(value)
14258
+ ? value
14259
+ : {};
14260
+ }
14261
+ buildQueryContext(queryContext, childParentField, parentResourceId) {
14262
+ return {
14263
+ ...(queryContext || {}),
14264
+ filters: {
14265
+ ...(queryContext?.filters || {}),
14266
+ [childParentField]: parentResourceId,
14267
+ },
14268
+ meta: {
14269
+ ...(queryContext?.meta || {}),
14270
+ relatedResource: true,
14271
+ parentFilterField: childParentField,
14272
+ },
14273
+ };
14274
+ }
14275
+ resolveParentResourceId(request, relatedResource) {
14276
+ if (request.parentResourceId != null) {
14277
+ return request.parentResourceId;
14278
+ }
14279
+ return this.readPath(request.parentRecord, relatedResource.parentIdPathVariable);
14280
+ }
14281
+ readPath(record, path) {
14282
+ if (!record || !path) {
14283
+ return null;
14284
+ }
14285
+ const value = path.split('.').reduce((current, segment) => {
14286
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
14287
+ return undefined;
14288
+ }
14289
+ return current[segment];
14290
+ }, record);
14291
+ return typeof value === 'string' || typeof value === 'number' ? value : null;
14292
+ }
14293
+ isCompleteRelatedResource(relatedResource) {
14294
+ return !!relatedResource
14295
+ && !!this.trim(relatedResource.childResourceKey)
14296
+ && !!this.trim(relatedResource.childResourcePath)
14297
+ && !!this.trim(relatedResource.childParentField)
14298
+ && !!this.trim(relatedResource.parentIdPathVariable)
14299
+ && Array.isArray(relatedResource.childOperations);
14300
+ }
14301
+ hasReadOperation(relatedResource) {
14302
+ return relatedResource.childOperations.includes('LIST')
14303
+ || relatedResource.childOperations.includes('FILTER');
14304
+ }
14305
+ buildStableTableId(surface, relatedResource, parentResourceId) {
14306
+ return this.sanitizeStableId(`${relatedResource.childResourceKey}.${surface.id}.${String(parentResourceId)}`);
14307
+ }
14308
+ normalizeResourcePath(resourcePath) {
14309
+ let normalized = this.trim(resourcePath);
14310
+ if (/^https?:\/\//i.test(normalized)) {
14311
+ try {
14312
+ normalized = new URL(normalized).pathname;
14313
+ }
14314
+ catch {
14315
+ return '';
14316
+ }
14317
+ }
14318
+ return normalized
14319
+ .replace(/^\/+/, '')
14320
+ .replace(/^(?:api\/)+/i, '')
14321
+ .replace(/\/+$/, '');
14322
+ }
14323
+ sanitizeStableId(value) {
14324
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '-');
14325
+ }
14326
+ humanizeResourceKey(value) {
14327
+ const lastSegment = this.trim(value).split(/[./_-]+/).filter(Boolean).pop() || 'registros';
14328
+ return lastSegment
14329
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
14330
+ .replace(/\s+/g, ' ')
14331
+ .trim();
14332
+ }
14333
+ t(key, fallback, params) {
14334
+ if (this.i18n) {
14335
+ return this.interpolate(this.i18n.t(key, params, fallback, RELATED_RESOURCE_OUTLET_I18N_NAMESPACE), params);
14336
+ }
14337
+ return this.interpolate(fallback, params);
14338
+ }
14339
+ interpolate(template, params) {
14340
+ return Object.entries(params || {}).reduce((current, [name, value]) => current.replace(new RegExp(`\\{${name}\\}`, 'g'), String(value ?? '')), template);
14341
+ }
14342
+ trim(value) {
14343
+ return typeof value === 'string' ? value.trim() : '';
14344
+ }
14345
+ clone(value) {
14346
+ return value == null ? value : JSON.parse(JSON.stringify(value));
14347
+ }
14348
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RelatedResourceSurfaceResolverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
14349
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RelatedResourceSurfaceResolverService, providedIn: 'any' });
14350
+ }
14351
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RelatedResourceSurfaceResolverService, decorators: [{
14352
+ type: Injectable,
14353
+ args: [{ providedIn: 'any' }]
14354
+ }] });
14355
+
14024
14356
  class ResourceSurfaceOpenAdapterService {
14025
14357
  discovery = inject(ResourceDiscoveryService);
14358
+ relatedResourceResolver = inject(RelatedResourceSurfaceResolverService);
14026
14359
  toPayload(surface, options) {
14027
14360
  const resourcePath = this.normalizeResourcePath(options.resourcePath);
14028
14361
  if (!resourcePath) {
@@ -14035,6 +14368,25 @@ class ResourceSurfaceOpenAdapterService {
14035
14368
  }
14036
14369
  : undefined;
14037
14370
  const resolvedApiEntry = this.discovery.resolveApiEntry(discoveryOptions);
14371
+ const icon = options.icon ?? this.resolveDefaultIcon(surface);
14372
+ if (surface.relatedResource) {
14373
+ const resolution = this.relatedResourceResolver.resolve({
14374
+ surface,
14375
+ parentResourceId: options.resourceId ?? null,
14376
+ parentResourcePath: resourcePath,
14377
+ presentation: options.presentation,
14378
+ title: options.title ?? surface.title,
14379
+ subtitle: options.subtitle ?? surface.description ?? null,
14380
+ icon,
14381
+ queryContext: options.queryContext,
14382
+ apiEndpointKey: options.endpointKey ?? null,
14383
+ apiUrlEntry: options.apiUrlEntry ?? resolvedApiEntry,
14384
+ });
14385
+ if (resolution.state !== 'ready' || !resolution.payload) {
14386
+ throw new Error(`ResourceSurfaceOpenAdapterService could not materialize related surface "${surface.id}": ${resolution.reason || resolution.state}.`);
14387
+ }
14388
+ return resolution.payload;
14389
+ }
14038
14390
  const resolvedSchemaUrl = this.discovery.resolveHref(surface.schemaUrl, discoveryOptions);
14039
14391
  const resolvedSubmitUrl = this.isWritableFormSurface(surface.kind)
14040
14392
  ? this.discovery.resolveHref(surface.path, discoveryOptions)
@@ -14046,7 +14398,7 @@ class ResourceSurfaceOpenAdapterService {
14046
14398
  presentation: options.presentation ?? basePayload.presentation,
14047
14399
  title: options.title ?? surface.title ?? basePayload.title,
14048
14400
  subtitle: options.subtitle ?? surface.description ?? basePayload.subtitle,
14049
- icon: options.icon ?? basePayload.icon,
14401
+ icon: icon ?? basePayload.icon,
14050
14402
  context: {
14051
14403
  resource: {
14052
14404
  resourceKey: surface.resourceKey,
@@ -14135,6 +14487,15 @@ class ResourceSurfaceOpenAdapterService {
14135
14487
  }
14136
14488
  return this.clone(preset.payload);
14137
14489
  }
14490
+ resolveDefaultIcon(surface) {
14491
+ if (surface.relatedResource) {
14492
+ return 'dataset_linked';
14493
+ }
14494
+ if (surface.kind === 'VIEW' || surface.kind === 'READ_PROJECTION') {
14495
+ return 'visibility';
14496
+ }
14497
+ return surface.scope === 'COLLECTION' ? 'add' : 'edit_note';
14498
+ }
14138
14499
  buildStableInstanceId(surface) {
14139
14500
  return `${surface.resourceKey}.${surface.id}`.replace(/[^a-zA-Z0-9._-]+/g, '-');
14140
14501
  }
@@ -14959,558 +15320,226 @@ class PraxisRuntimeComponentObservationRegistryService {
14959
15320
  return clonePraxisRuntimeComponentObservation(observation);
14960
15321
  }
14961
15322
  isActiveObservation(observation) {
14962
- if (!observation.lifecycle.active) {
14963
- return false;
14964
- }
14965
- const ttlMs = observation.lifecycle.ttlMs;
14966
- if (ttlMs === undefined || ttlMs < 0) {
14967
- return true;
14968
- }
14969
- const capturedAt = Date.parse(observation.lifecycle.capturedAt);
14970
- return Number.isFinite(capturedAt) && Date.now() - capturedAt <= ttlMs;
14971
- }
14972
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisRuntimeComponentObservationRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
14973
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisRuntimeComponentObservationRegistryService, providedIn: 'root' });
14974
- }
14975
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisRuntimeComponentObservationRegistryService, decorators: [{
14976
- type: Injectable,
14977
- args: [{ providedIn: 'root' }]
14978
- }] });
14979
- function registerPraxisRuntimeComponentObservation(provider, options = {}) {
14980
- const registry = inject(PraxisRuntimeComponentObservationRegistryService);
14981
- const destroyRef = options.destroyRef ?? inject(DestroyRef);
14982
- return registry.register(provider, { ...options, destroyRef });
14983
- }
14984
-
14985
- class ResourceActionOpenAdapterService {
14986
- discovery = inject(ResourceDiscoveryService);
14987
- toPayload(action, options) {
14988
- const resourcePath = this.normalizeResourcePath(options.resourcePath);
14989
- if (!resourcePath) {
14990
- throw new Error('ResourceActionOpenAdapterService requires resourcePath.');
14991
- }
14992
- if (!action.requestSchemaUrl) {
14993
- throw new Error(`ResourceActionOpenAdapterService requires requestSchemaUrl for action "${action.id}".`);
14994
- }
14995
- const discoveryOptions = options.endpointKey || options.apiUrlEntry
14996
- ? {
14997
- ...(options.endpointKey ? { endpointKey: options.endpointKey } : {}),
14998
- ...(options.apiUrlEntry ? { apiUrlEntry: options.apiUrlEntry } : {}),
14999
- }
15000
- : undefined;
15001
- const resolvedRequestSchemaUrl = this.discovery.resolveHref(action.requestSchemaUrl, discoveryOptions);
15002
- const resolvedResponseSchemaUrl = action.responseSchemaUrl
15003
- ? this.discovery.resolveHref(action.responseSchemaUrl, discoveryOptions)
15004
- : null;
15005
- const resolvedSubmitUrl = this.discovery.resolveHref(action.path, discoveryOptions);
15006
- const payload = this.clone(this.resolveDynamicFormPreset());
15007
- payload.presentation = options.presentation ?? payload.presentation;
15008
- payload.title = options.title ?? action.title ?? payload.title;
15009
- payload.subtitle = options.subtitle ?? action.description ?? payload.subtitle;
15010
- payload.icon = options.icon ?? payload.icon;
15011
- payload.context = {
15012
- resource: {
15013
- resourceKey: action.resourceKey,
15014
- resourcePath,
15015
- resourceId: options.resourceId ?? null,
15016
- group: options.group ?? null,
15017
- },
15018
- action: {
15019
- id: action.id,
15020
- scope: action.scope,
15021
- operationId: action.operationId,
15022
- path: action.path,
15023
- method: action.method,
15024
- requestSchemaId: action.requestSchemaId ?? null,
15025
- requestSchemaUrl: action.requestSchemaUrl,
15026
- responseSchemaId: action.responseSchemaId ?? null,
15027
- responseSchemaUrl: action.responseSchemaUrl ?? null,
15028
- availability: action.availability,
15029
- successMessage: action.successMessage ?? null,
15030
- tags: action.tags,
15031
- execution: action.execution ?? null,
15032
- },
15033
- };
15034
- payload.widget.inputs = {
15035
- ...(payload.widget.inputs || {}),
15036
- resourcePath,
15037
- formId: this.buildStableInstanceId(action),
15038
- mode: 'create',
15039
- configPersistenceStrategy: 'input-first',
15040
- layoutPolicy: {
15041
- source: 'schema',
15042
- intent: 'command',
15043
- preset: 'groupedCommand',
15044
- lifecycle: 'live',
15045
- persistence: 'transient',
15046
- schemaType: 'request',
15047
- },
15048
- schemaUrl: resolvedRequestSchemaUrl,
15049
- apiEndpointKey: options.endpointKey ?? null,
15050
- apiUrlEntry: options.apiUrlEntry ?? (discoveryOptions ? this.discovery.resolveApiEntry(discoveryOptions) : null),
15051
- submitMethod: String(action.method || '').trim().toLowerCase(),
15052
- submitUrl: resolvedSubmitUrl,
15053
- responseSchemaUrl: resolvedResponseSchemaUrl,
15054
- };
15055
- if (options.initialValue) {
15056
- payload.widget.inputs['initialValue'] = this.clone(options.initialValue);
15057
- }
15058
- if (action.scope === 'ITEM') {
15059
- if (options.resourceId != null) {
15060
- payload.widget.inputs['resourceId'] = options.resourceId;
15061
- }
15062
- else if (options.idBindingPath) {
15063
- payload.bindings = [
15064
- ...(payload.bindings || []),
15065
- this.buildIdBinding(options.idBindingPath),
15066
- ];
15067
- }
15068
- else {
15069
- throw new Error(`ResourceActionOpenAdapterService requires resourceId or idBindingPath for item action "${action.id}".`);
15070
- }
15071
- }
15072
- this.applyExecutionInputs(payload, action, options);
15073
- return payload;
15074
- }
15075
- resolveDynamicFormPreset() {
15076
- const preset = SURFACE_OPEN_PRESETS.find((candidate) => candidate.id === 'praxis-dynamic-form');
15077
- if (!preset) {
15078
- throw new Error('Missing canonical surface preset "praxis-dynamic-form".');
15079
- }
15080
- return preset.payload;
15081
- }
15082
- buildStableInstanceId(action) {
15083
- return `${action.resourceKey}.action.${action.id}`.replace(/[^a-zA-Z0-9._-]+/g, '-');
15084
- }
15085
- applyExecutionInputs(payload, action, options) {
15086
- const execution = action.execution;
15087
- if (!execution) {
15088
- payload.widget.inputs['submitIdempotencyKey'] = this.createCommandIdentity('idempotency');
15089
- return;
15090
- }
15091
- const inputs = payload.widget.inputs;
15092
- if (execution.preconditions.idempotencyKey !== 'NONE') {
15093
- inputs['submitIdempotencyKey'] = this.createCommandIdentity('idempotency');
15094
- }
15095
- if (execution.preconditions.correlationId !== 'NONE') {
15096
- inputs['submitCorrelationId'] =
15097
- String(options.correlationId ?? '').trim() || this.createCommandIdentity('correlation');
15098
- }
15099
- if (execution.preconditions.resourceVersionTransport !== 'IF_MATCH') {
15100
- return;
15101
- }
15102
- if (options.resourceVersion != null && String(options.resourceVersion).trim()) {
15103
- inputs['submitResourceVersion'] = options.resourceVersion;
15104
- return;
15105
- }
15106
- if (options.resourceVersionBindingPath) {
15107
- payload.bindings = [
15108
- ...(payload.bindings || []),
15109
- {
15110
- from: options.resourceVersionBindingPath,
15111
- to: 'widget.inputs.submitResourceVersion',
15112
- mode: 'path',
15113
- },
15114
- ];
15115
- return;
15116
- }
15117
- if (execution.preconditions.resourceVersion === 'REQUIRED') {
15118
- throw new Error(`ResourceActionOpenAdapterService requires resourceVersion or resourceVersionBindingPath for action "${action.id}".`);
15119
- }
15120
- }
15121
- createIdempotencyKey() {
15122
- const randomUuid = globalThis.crypto?.randomUUID?.bind(globalThis.crypto);
15123
- return randomUuid ? randomUuid() : `praxis-action-${Date.now()}-${Math.random().toString(16).slice(2)}`;
15124
- }
15125
- createCommandIdentity(kind) {
15126
- return `${kind}-${this.createIdempotencyKey()}`;
15127
- }
15128
- normalizeResourcePath(resourcePath) {
15129
- return String(resourcePath || '').trim().replace(/^\/+/, '').replace(/\/+$/, '');
15130
- }
15131
- buildIdBinding(idBindingPath) {
15132
- return {
15133
- from: idBindingPath,
15134
- to: 'widget.inputs.resourceId',
15135
- mode: 'path',
15136
- };
15137
- }
15138
- clone(value) {
15139
- return value == null ? value : JSON.parse(JSON.stringify(value));
15140
- }
15141
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ResourceActionOpenAdapterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15142
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ResourceActionOpenAdapterService, providedIn: 'any' });
15143
- }
15144
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ResourceActionOpenAdapterService, decorators: [{
15145
- type: Injectable,
15146
- args: [{ providedIn: 'any' }]
15147
- }] });
15148
-
15149
- class SurfaceOutletRegistryService {
15150
- registrations = new Map();
15151
- register(registration) {
15152
- const key = this.key(registration.resourceKey, registration.surfaceId);
15153
- this.registrations.set(key, [...(this.registrations.get(key) ?? []), registration]
15154
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)));
15155
- return () => {
15156
- const next = (this.registrations.get(key) ?? []).filter((item) => item !== registration);
15157
- next.length ? this.registrations.set(key, next) : this.registrations.delete(key);
15158
- };
15159
- }
15160
- async tryActivate(payload, context) {
15161
- const resourceKey = String(payload.context?.['surface']?.['resourceKey'] ?? '').trim();
15162
- const surfaceId = String(payload.context?.['surface']?.['id'] ?? '').trim();
15163
- if (!resourceKey || !surfaceId)
15323
+ if (!observation.lifecycle.active) {
15164
15324
  return false;
15165
- for (const registration of this.registrations.get(this.key(resourceKey, surfaceId)) ?? []) {
15166
- if (await registration.activate(payload, context))
15167
- return true;
15168
15325
  }
15169
- return false;
15170
- }
15171
- key(resourceKey, surfaceId) {
15172
- return `${resourceKey.trim().toLowerCase()}::${surfaceId.trim().toLowerCase()}`;
15326
+ const ttlMs = observation.lifecycle.ttlMs;
15327
+ if (ttlMs === undefined || ttlMs < 0) {
15328
+ return true;
15329
+ }
15330
+ const capturedAt = Date.parse(observation.lifecycle.capturedAt);
15331
+ return Number.isFinite(capturedAt) && Date.now() - capturedAt <= ttlMs;
15173
15332
  }
15174
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceOutletRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15175
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceOutletRegistryService, providedIn: 'root' });
15333
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisRuntimeComponentObservationRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15334
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisRuntimeComponentObservationRegistryService, providedIn: 'root' });
15176
15335
  }
15177
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceOutletRegistryService, decorators: [{
15336
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisRuntimeComponentObservationRegistryService, decorators: [{
15178
15337
  type: Injectable,
15179
15338
  args: [{ providedIn: 'root' }]
15180
15339
  }] });
15340
+ function registerPraxisRuntimeComponentObservation(provider, options = {}) {
15341
+ const registry = inject(PraxisRuntimeComponentObservationRegistryService);
15342
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
15343
+ return registry.register(provider, { ...options, destroyRef });
15344
+ }
15181
15345
 
15182
- const RELATED_RESOURCE_OUTLET_I18N_NAMESPACE = 'relatedResourceOutlet';
15183
- const RELATED_RESOURCE_OUTLET_I18N_CONFIG = {
15184
- namespaces: {
15185
- [RELATED_RESOURCE_OUTLET_I18N_NAMESPACE]: {
15186
- 'pt-BR': {
15187
- 'state.idle.title': 'Recurso relacionado não selecionado',
15188
- 'state.idle.description': 'Selecione uma surface relacionada para carregar os dados.',
15189
- 'state.resolving.title': 'Resolvendo recurso relacionado',
15190
- 'state.resolving.description': 'Validando metadados e permissões da relação.',
15191
- 'state.loading.title': 'Carregando recurso relacionado',
15192
- 'state.loading.description': 'Aguarde enquanto a coleção relacionada é carregada.',
15193
- 'state.empty.title': 'Nenhuma operação de leitura publicada',
15194
- 'state.empty.description': 'A relação existe, mas não há operação de lista ou filtro disponível.',
15195
- 'state.permission-limited.title': 'Acesso limitado',
15196
- 'state.permission-limited.description': 'O contexto atual não permite abrir este recurso relacionado.',
15197
- 'state.not-found.title': 'Relação indisponível',
15198
- 'state.not-found.description': 'Não foi possível resolver a relação ou o identificador do registro pai.',
15199
- 'state.error.title': 'Falha ao preparar recurso relacionado',
15200
- 'state.error.description': 'Ocorreu um erro ao preparar a superfície relacionada.',
15201
- 'emptyState.related.title': 'Sem registros em {label}',
15202
- 'emptyState.related.description': 'Esta coleção relacionada não possui registros para o contexto selecionado.',
15203
- 'emptyState.related.descriptionWithAction': 'Use a ação principal para adicionar um registro relacionado quando houver informações para registrar.',
15204
- 'emptyState.related.action.create': 'Adicionar registro',
15205
- 'action.open': 'Abrir relacionado',
15206
- 'status.ready': 'Recurso relacionado pronto',
15207
- },
15208
- 'en-US': {
15209
- 'state.idle.title': 'Related resource not selected',
15210
- 'state.idle.description': 'Select a related surface to load its data.',
15211
- 'state.resolving.title': 'Resolving related resource',
15212
- 'state.resolving.description': 'Validating relation metadata and permissions.',
15213
- 'state.loading.title': 'Loading related resource',
15214
- 'state.loading.description': 'Wait while the related collection is loaded.',
15215
- 'state.empty.title': 'No read operation published',
15216
- 'state.empty.description': 'The relation exists, but no list or filter operation is available.',
15217
- 'state.permission-limited.title': 'Limited access',
15218
- 'state.permission-limited.description': 'The current context cannot open this related resource.',
15219
- 'state.not-found.title': 'Relation unavailable',
15220
- 'state.not-found.description': 'The relation or parent record identifier could not be resolved.',
15221
- 'state.error.title': 'Failed to prepare related resource',
15222
- 'state.error.description': 'An error occurred while preparing the related surface.',
15223
- 'emptyState.related.title': 'No records in {label}',
15224
- 'emptyState.related.description': 'This related collection has no records for the selected context.',
15225
- 'emptyState.related.descriptionWithAction': 'Use the primary action to add a related record when there is information to capture.',
15226
- 'emptyState.related.action.create': 'Add record',
15227
- 'action.open': 'Open related',
15228
- 'status.ready': 'Related resource ready',
15229
- },
15230
- },
15231
- },
15232
- };
15233
-
15234
- class RelatedResourceSurfaceResolverService {
15235
- i18n = inject(PraxisI18nService, { optional: true });
15236
- resolve(request) {
15237
- try {
15238
- if (!request?.surface) {
15239
- return { state: 'idle', reason: 'surface-not-provided' };
15240
- }
15241
- const surface = request.surface;
15242
- if (surface.availability?.allowed === false) {
15243
- return {
15244
- state: 'permission-limited',
15245
- reason: surface.availability.reason || 'surface-not-allowed',
15246
- surface,
15247
- relatedResource: surface.relatedResource ?? null,
15248
- };
15249
- }
15250
- const relatedResource = surface.relatedResource ?? null;
15251
- if (!this.isCompleteRelatedResource(relatedResource)) {
15252
- return {
15253
- state: 'not-found',
15254
- reason: 'related-resource-not-published',
15255
- surface,
15256
- relatedResource,
15257
- };
15258
- }
15259
- if (!this.hasReadOperation(relatedResource)) {
15260
- return {
15261
- state: 'empty',
15262
- reason: 'related-resource-read-operation-not-published',
15263
- surface,
15264
- relatedResource,
15265
- };
15266
- }
15267
- const parentResourceId = this.resolveParentResourceId(request, relatedResource);
15268
- if (parentResourceId == null || parentResourceId === '') {
15269
- return {
15270
- state: 'not-found',
15271
- reason: 'parent-resource-id-not-resolved',
15272
- surface,
15273
- relatedResource,
15274
- };
15275
- }
15276
- const queryContext = this.buildQueryContext(request.queryContext, relatedResource.childParentField, parentResourceId);
15277
- const payload = this.buildPayload(surface, relatedResource, parentResourceId, queryContext, request);
15278
- return {
15279
- state: 'ready',
15280
- surface,
15281
- relatedResource,
15282
- parentResourceId,
15283
- childResourcePath: relatedResource.childResourcePath,
15284
- childResourceKey: relatedResource.childResourceKey,
15285
- queryContext,
15286
- payload,
15287
- };
15288
- }
15289
- catch (error) {
15290
- return {
15291
- state: 'error',
15292
- reason: error instanceof Error ? error.message : 'related-resource-resolution-failed',
15293
- surface: request?.surface ?? null,
15294
- relatedResource: request?.surface?.relatedResource ?? null,
15295
- };
15346
+ class ResourceActionOpenAdapterService {
15347
+ discovery = inject(ResourceDiscoveryService);
15348
+ toPayload(action, options) {
15349
+ const resourcePath = this.normalizeResourcePath(options.resourcePath);
15350
+ if (!resourcePath) {
15351
+ throw new Error('ResourceActionOpenAdapterService requires resourcePath.');
15296
15352
  }
15297
- }
15298
- state(state, reason) {
15299
- return { state, reason };
15300
- }
15301
- buildPayload(surface, relatedResource, parentResourceId, queryContext, request) {
15302
- const preset = SURFACE_OPEN_PRESETS.find((candidate) => candidate.id === 'praxis-table');
15303
- if (!preset) {
15304
- throw new Error('Missing canonical surface preset "praxis-table".');
15353
+ if (!action.requestSchemaUrl) {
15354
+ throw new Error(`ResourceActionOpenAdapterService requires requestSchemaUrl for action "${action.id}".`);
15305
15355
  }
15306
- const payload = this.clone(preset.payload);
15307
- payload.presentation = request.presentation ?? payload.presentation;
15308
- payload.title = request.title || surface.title;
15309
- payload.subtitle = request.subtitle || surface.description || undefined;
15310
- payload.icon = request.icon || payload.icon;
15311
- payload.widget.inputs = {
15312
- ...(payload.widget.inputs || {}),
15313
- configPersistenceStrategy: 'volatile',
15314
- resourcePath: this.normalizeResourcePath(relatedResource.childResourcePath),
15315
- apiEndpointKey: request.apiEndpointKey ?? null,
15316
- apiUrlEntry: request.apiUrlEntry ?? null,
15317
- tableId: request.tableId || this.buildStableTableId(surface, relatedResource, parentResourceId),
15318
- config: this.buildTableConfig(surface, relatedResource, request),
15319
- queryContext,
15320
- enableCustomization: request.enableCustomization === true,
15321
- ...(this.trim(request.authoringCapability)
15322
- ? { authoringCapability: this.trim(request.authoringCapability) }
15323
- : {}),
15324
- };
15325
- payload.context = {
15326
- ...(payload.context || {}),
15327
- resource: {
15328
- resourceKey: surface.resourceKey,
15329
- resourcePath: this.normalizeResourcePath(request.parentResourcePath || ''),
15330
- resourceId: parentResourceId,
15331
- },
15332
- surface,
15333
- relatedResource,
15334
- childResource: {
15335
- resourceKey: relatedResource.childResourceKey,
15336
- resourcePath: this.normalizeResourcePath(relatedResource.childResourcePath),
15337
- parentField: relatedResource.childParentField,
15338
- selectable: relatedResource.selectable,
15339
- selectionKeyField: relatedResource.selectionKeyField,
15340
- operations: relatedResource.childOperations,
15341
- },
15342
- };
15343
- return payload;
15344
- }
15345
- buildTableConfig(surface, relatedResource, request) {
15346
- const tableConfig = request.tableConfig;
15347
- const emptyState = request.emptyState;
15348
- const hasTableConfig = !!tableConfig && typeof tableConfig === 'object' && !Array.isArray(tableConfig);
15349
- const hasEmptyState = !!emptyState && typeof emptyState === 'object' && !Array.isArray(emptyState);
15350
- const base = hasTableConfig ? tableConfig : {};
15351
- const toolbar = this.objectValue(base['toolbar']);
15352
- const columnsVisibility = this.objectValue(toolbar['columnsVisibility']);
15353
- const behavior = this.objectValue(base['behavior']);
15354
- const currentEmptyState = this.objectValue(behavior['emptyState']);
15355
- const hasCurrentEmptyState = Object.keys(currentEmptyState).length > 0;
15356
- const resolvedEmptyState = hasEmptyState
15356
+ const discoveryOptions = options.endpointKey || options.apiUrlEntry
15357
15357
  ? {
15358
- ...currentEmptyState,
15359
- ...emptyState,
15358
+ ...(options.endpointKey ? { endpointKey: options.endpointKey } : {}),
15359
+ ...(options.apiUrlEntry ? { apiUrlEntry: options.apiUrlEntry } : {}),
15360
15360
  }
15361
- : hasCurrentEmptyState
15362
- ? currentEmptyState
15363
- : this.buildRelatedEmptyState(surface, relatedResource, request);
15364
- if (!hasTableConfig && !resolvedEmptyState) {
15365
- return undefined;
15366
- }
15367
- return {
15368
- ...base,
15369
- toolbar: {
15370
- ...toolbar,
15371
- columnsVisibility: {
15372
- enabled: false,
15373
- ...columnsVisibility,
15374
- },
15361
+ : undefined;
15362
+ const resolvedRequestSchemaUrl = this.discovery.resolveHref(action.requestSchemaUrl, discoveryOptions);
15363
+ const resolvedResponseSchemaUrl = action.responseSchemaUrl
15364
+ ? this.discovery.resolveHref(action.responseSchemaUrl, discoveryOptions)
15365
+ : null;
15366
+ const resolvedSubmitUrl = this.discovery.resolveHref(action.path, discoveryOptions);
15367
+ const payload = this.clone(this.resolveDynamicFormPreset());
15368
+ payload.presentation = options.presentation ?? payload.presentation;
15369
+ payload.title = options.title ?? action.title ?? payload.title;
15370
+ payload.subtitle = options.subtitle ?? action.description ?? payload.subtitle;
15371
+ payload.icon = options.icon ?? payload.icon;
15372
+ payload.context = {
15373
+ resource: {
15374
+ resourceKey: action.resourceKey,
15375
+ resourcePath,
15376
+ resourceId: options.resourceId ?? null,
15377
+ group: options.group ?? null,
15375
15378
  },
15376
- behavior: {
15377
- ...behavior,
15378
- emptyState: resolvedEmptyState,
15379
+ action: {
15380
+ id: action.id,
15381
+ scope: action.scope,
15382
+ operationId: action.operationId,
15383
+ path: action.path,
15384
+ method: action.method,
15385
+ requestSchemaId: action.requestSchemaId ?? null,
15386
+ requestSchemaUrl: action.requestSchemaUrl,
15387
+ responseSchemaId: action.responseSchemaId ?? null,
15388
+ responseSchemaUrl: action.responseSchemaUrl ?? null,
15389
+ availability: action.availability,
15390
+ successMessage: action.successMessage ?? null,
15391
+ tags: action.tags,
15392
+ execution: action.execution ?? null,
15379
15393
  },
15380
15394
  };
15381
- }
15382
- buildRelatedEmptyState(surface, relatedResource, request) {
15383
- const label = this.trim(request.title)
15384
- || this.trim(surface.title)
15385
- || this.humanizeResourceKey(relatedResource.childResourceKey);
15386
- const canCreate = relatedResource.childOperations.includes('CREATE');
15387
- const descriptionKey = canCreate
15388
- ? 'emptyState.related.descriptionWithAction'
15389
- : 'emptyState.related.description';
15390
- const actions = canCreate
15391
- ? [
15392
- {
15393
- label: this.t('emptyState.related.action.create', 'Adicionar registro'),
15394
- action: 'create',
15395
- icon: 'add',
15396
- primary: true,
15397
- },
15398
- ]
15399
- : [];
15400
- return {
15401
- title: this.t('emptyState.related.title', 'Sem registros em {label}', { label }),
15402
- message: this.t(descriptionKey, canCreate
15403
- ? 'Use a ação principal para adicionar um registro relacionado quando houver informações para registrar.'
15404
- : 'Esta coleção relacionada não possui registros para o contexto selecionado.', { label }),
15405
- icon: this.trim(request.icon) || 'hub',
15406
- tone: 'neutral',
15407
- variant: 'inline',
15408
- density: 'compact',
15409
- alignment: 'center',
15410
- iconContainer: 'soft',
15411
- actions,
15412
- };
15413
- }
15414
- objectValue(value) {
15415
- return value && typeof value === 'object' && !Array.isArray(value)
15416
- ? value
15417
- : {};
15418
- }
15419
- buildQueryContext(queryContext, childParentField, parentResourceId) {
15420
- return {
15421
- ...(queryContext || {}),
15422
- filters: {
15423
- ...(queryContext?.filters || {}),
15424
- [childParentField]: parentResourceId,
15425
- },
15426
- meta: {
15427
- ...(queryContext?.meta || {}),
15428
- relatedResource: true,
15429
- parentFilterField: childParentField,
15395
+ payload.widget.inputs = {
15396
+ ...(payload.widget.inputs || {}),
15397
+ resourcePath,
15398
+ formId: this.buildStableInstanceId(action),
15399
+ mode: 'create',
15400
+ configPersistenceStrategy: 'input-first',
15401
+ layoutPolicy: {
15402
+ source: 'schema',
15403
+ intent: 'command',
15404
+ preset: 'groupedCommand',
15405
+ lifecycle: 'live',
15406
+ persistence: 'transient',
15407
+ schemaType: 'request',
15430
15408
  },
15409
+ schemaUrl: resolvedRequestSchemaUrl,
15410
+ apiEndpointKey: options.endpointKey ?? null,
15411
+ apiUrlEntry: options.apiUrlEntry ?? (discoveryOptions ? this.discovery.resolveApiEntry(discoveryOptions) : null),
15412
+ submitMethod: String(action.method || '').trim().toLowerCase(),
15413
+ submitUrl: resolvedSubmitUrl,
15414
+ responseSchemaUrl: resolvedResponseSchemaUrl,
15431
15415
  };
15432
- }
15433
- resolveParentResourceId(request, relatedResource) {
15434
- if (request.parentResourceId != null) {
15435
- return request.parentResourceId;
15436
- }
15437
- return this.readPath(request.parentRecord, relatedResource.parentIdPathVariable);
15438
- }
15439
- readPath(record, path) {
15440
- if (!record || !path) {
15441
- return null;
15416
+ if (options.initialValue) {
15417
+ payload.widget.inputs['initialValue'] = this.clone(options.initialValue);
15442
15418
  }
15443
- const value = path.split('.').reduce((current, segment) => {
15444
- if (!current || typeof current !== 'object' || Array.isArray(current)) {
15445
- return undefined;
15419
+ if (action.scope === 'ITEM') {
15420
+ if (options.resourceId != null) {
15421
+ payload.widget.inputs['resourceId'] = options.resourceId;
15446
15422
  }
15447
- return current[segment];
15448
- }, record);
15449
- return typeof value === 'string' || typeof value === 'number' ? value : null;
15450
- }
15451
- isCompleteRelatedResource(relatedResource) {
15452
- return !!relatedResource
15453
- && !!this.trim(relatedResource.childResourceKey)
15454
- && !!this.trim(relatedResource.childResourcePath)
15455
- && !!this.trim(relatedResource.childParentField)
15456
- && !!this.trim(relatedResource.parentIdPathVariable)
15457
- && Array.isArray(relatedResource.childOperations);
15458
- }
15459
- hasReadOperation(relatedResource) {
15460
- return relatedResource.childOperations.includes('LIST')
15461
- || relatedResource.childOperations.includes('FILTER');
15462
- }
15463
- buildStableTableId(surface, relatedResource, parentResourceId) {
15464
- return this.sanitizeStableId(`${relatedResource.childResourceKey}.${surface.id}.${String(parentResourceId)}`);
15465
- }
15466
- normalizeResourcePath(resourcePath) {
15467
- let normalized = this.trim(resourcePath);
15468
- if (/^https?:\/\//i.test(normalized)) {
15469
- try {
15470
- normalized = new URL(normalized).pathname;
15423
+ else if (options.idBindingPath) {
15424
+ payload.bindings = [
15425
+ ...(payload.bindings || []),
15426
+ this.buildIdBinding(options.idBindingPath),
15427
+ ];
15471
15428
  }
15472
- catch {
15473
- return '';
15429
+ else {
15430
+ throw new Error(`ResourceActionOpenAdapterService requires resourceId or idBindingPath for item action "${action.id}".`);
15474
15431
  }
15475
15432
  }
15476
- return normalized
15477
- .replace(/^\/+/, '')
15478
- .replace(/^(?:api\/)+/i, '')
15479
- .replace(/\/+$/, '');
15433
+ this.applyExecutionInputs(payload, action, options);
15434
+ return payload;
15480
15435
  }
15481
- sanitizeStableId(value) {
15482
- return value.replace(/[^a-zA-Z0-9._-]+/g, '-');
15436
+ resolveDynamicFormPreset() {
15437
+ const preset = SURFACE_OPEN_PRESETS.find((candidate) => candidate.id === 'praxis-dynamic-form');
15438
+ if (!preset) {
15439
+ throw new Error('Missing canonical surface preset "praxis-dynamic-form".');
15440
+ }
15441
+ return preset.payload;
15483
15442
  }
15484
- humanizeResourceKey(value) {
15485
- const lastSegment = this.trim(value).split(/[./_-]+/).filter(Boolean).pop() || 'registros';
15486
- return lastSegment
15487
- .replace(/([a-z])([A-Z])/g, '$1 $2')
15488
- .replace(/\s+/g, ' ')
15489
- .trim();
15443
+ buildStableInstanceId(action) {
15444
+ return `${action.resourceKey}.action.${action.id}`.replace(/[^a-zA-Z0-9._-]+/g, '-');
15490
15445
  }
15491
- t(key, fallback, params) {
15492
- if (this.i18n) {
15493
- return this.interpolate(this.i18n.t(key, params, fallback, RELATED_RESOURCE_OUTLET_I18N_NAMESPACE), params);
15446
+ applyExecutionInputs(payload, action, options) {
15447
+ const execution = action.execution;
15448
+ if (!execution) {
15449
+ payload.widget.inputs['submitIdempotencyKey'] = this.createCommandIdentity('idempotency');
15450
+ return;
15451
+ }
15452
+ const inputs = payload.widget.inputs;
15453
+ if (execution.preconditions.idempotencyKey !== 'NONE') {
15454
+ inputs['submitIdempotencyKey'] = this.createCommandIdentity('idempotency');
15455
+ }
15456
+ if (execution.preconditions.correlationId !== 'NONE') {
15457
+ inputs['submitCorrelationId'] =
15458
+ String(options.correlationId ?? '').trim() || this.createCommandIdentity('correlation');
15459
+ }
15460
+ if (execution.preconditions.resourceVersionTransport !== 'IF_MATCH') {
15461
+ return;
15462
+ }
15463
+ if (options.resourceVersion != null && String(options.resourceVersion).trim()) {
15464
+ inputs['submitResourceVersion'] = options.resourceVersion;
15465
+ return;
15466
+ }
15467
+ if (options.resourceVersionBindingPath) {
15468
+ payload.bindings = [
15469
+ ...(payload.bindings || []),
15470
+ {
15471
+ from: options.resourceVersionBindingPath,
15472
+ to: 'widget.inputs.submitResourceVersion',
15473
+ mode: 'path',
15474
+ },
15475
+ ];
15476
+ return;
15477
+ }
15478
+ if (execution.preconditions.resourceVersion === 'REQUIRED') {
15479
+ throw new Error(`ResourceActionOpenAdapterService requires resourceVersion or resourceVersionBindingPath for action "${action.id}".`);
15494
15480
  }
15495
- return this.interpolate(fallback, params);
15496
15481
  }
15497
- interpolate(template, params) {
15498
- return Object.entries(params || {}).reduce((current, [name, value]) => current.replace(new RegExp(`\\{${name}\\}`, 'g'), String(value ?? '')), template);
15482
+ createIdempotencyKey() {
15483
+ const randomUuid = globalThis.crypto?.randomUUID?.bind(globalThis.crypto);
15484
+ return randomUuid ? randomUuid() : `praxis-action-${Date.now()}-${Math.random().toString(16).slice(2)}`;
15499
15485
  }
15500
- trim(value) {
15501
- return typeof value === 'string' ? value.trim() : '';
15486
+ createCommandIdentity(kind) {
15487
+ return `${kind}-${this.createIdempotencyKey()}`;
15488
+ }
15489
+ normalizeResourcePath(resourcePath) {
15490
+ return String(resourcePath || '').trim().replace(/^\/+/, '').replace(/\/+$/, '');
15491
+ }
15492
+ buildIdBinding(idBindingPath) {
15493
+ return {
15494
+ from: idBindingPath,
15495
+ to: 'widget.inputs.resourceId',
15496
+ mode: 'path',
15497
+ };
15502
15498
  }
15503
15499
  clone(value) {
15504
15500
  return value == null ? value : JSON.parse(JSON.stringify(value));
15505
15501
  }
15506
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RelatedResourceSurfaceResolverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15507
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RelatedResourceSurfaceResolverService, providedIn: 'any' });
15502
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ResourceActionOpenAdapterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15503
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ResourceActionOpenAdapterService, providedIn: 'any' });
15508
15504
  }
15509
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RelatedResourceSurfaceResolverService, decorators: [{
15505
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ResourceActionOpenAdapterService, decorators: [{
15510
15506
  type: Injectable,
15511
15507
  args: [{ providedIn: 'any' }]
15512
15508
  }] });
15513
15509
 
15510
+ class SurfaceOutletRegistryService {
15511
+ registrations = new Map();
15512
+ register(registration) {
15513
+ const key = this.key(registration.resourceKey, registration.surfaceId);
15514
+ this.registrations.set(key, [...(this.registrations.get(key) ?? []), registration]
15515
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)));
15516
+ return () => {
15517
+ const next = (this.registrations.get(key) ?? []).filter((item) => item !== registration);
15518
+ next.length ? this.registrations.set(key, next) : this.registrations.delete(key);
15519
+ };
15520
+ }
15521
+ async tryActivate(payload, context) {
15522
+ const resourceKey = String(payload.context?.['surface']?.['resourceKey'] ?? '').trim();
15523
+ const surfaceId = String(payload.context?.['surface']?.['id'] ?? '').trim();
15524
+ if (!resourceKey || !surfaceId)
15525
+ return false;
15526
+ for (const registration of this.registrations.get(this.key(resourceKey, surfaceId)) ?? []) {
15527
+ if (await registration.activate(payload, context))
15528
+ return true;
15529
+ }
15530
+ return false;
15531
+ }
15532
+ key(resourceKey, surfaceId) {
15533
+ return `${resourceKey.trim().toLowerCase()}::${surfaceId.trim().toLowerCase()}`;
15534
+ }
15535
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceOutletRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
15536
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceOutletRegistryService, providedIn: 'root' });
15537
+ }
15538
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SurfaceOutletRegistryService, decorators: [{
15539
+ type: Injectable,
15540
+ args: [{ providedIn: 'root' }]
15541
+ }] });
15542
+
15514
15543
  class SurfaceOpenMaterializerService {
15515
15544
  discovery = inject(ResourceDiscoveryService);
15516
15545
  async materialize(payload, context) {
@@ -15784,6 +15813,8 @@ class SurfaceOpenMaterializerService {
15784
15813
  path: '',
15785
15814
  schemaPath: schemaResourcePath || null,
15786
15815
  idField: this.resolveRelatedSelectionKeyField(payload),
15816
+ title: payload.title || null,
15817
+ formTitle: payload.title || null,
15787
15818
  endpointKey: previousInputs['apiEndpointKey'] ?? undefined,
15788
15819
  apiUrlEntry: previousInputs['apiUrlEntry'] ?? undefined,
15789
15820
  },
@@ -15793,12 +15824,6 @@ class SurfaceOpenMaterializerService {
15793
15824
  queryContext: null,
15794
15825
  defaults: {
15795
15826
  openMode: 'drawer',
15796
- modal: {
15797
- width: 'min(720px, 100vw)',
15798
- maxWidth: '100vw',
15799
- height: '100vh',
15800
- position: 'end',
15801
- },
15802
15827
  },
15803
15828
  actions,
15804
15829
  },
@@ -16029,12 +16054,14 @@ class SurfaceOpenMaterializerService {
16029
16054
  buildRelatedCrudActions(payload, operations, paths) {
16030
16055
  const noun = this.resolveRelatedActionNoun(payload);
16031
16056
  const formIdPrefix = this.stableSurfaceId(payload);
16057
+ const contextFields = this.buildRelatedContextFields(payload);
16032
16058
  const actions = [];
16033
16059
  if (operations.has('CREATE')) {
16034
16060
  actions.push({
16035
16061
  id: 'create',
16036
16062
  action: 'create',
16037
- label: `Novo ${noun}`,
16063
+ label: `Adicionar ${noun}`,
16064
+ tooltip: this.resolveRelatedActionDescription(payload, 'create', noun),
16038
16065
  formId: `${formIdPrefix}.create`,
16039
16066
  icon: 'add',
16040
16067
  color: 'primary',
@@ -16042,7 +16069,7 @@ class SurfaceOpenMaterializerService {
16042
16069
  position: 'end',
16043
16070
  target: { scope: 'collection' },
16044
16071
  openMode: 'drawer',
16045
- form: this.buildExplicitRelatedActionForm('create', paths),
16072
+ form: this.buildExplicitRelatedActionForm('create', paths, this.buildRelatedParentInitialValue(payload), contextFields),
16046
16073
  });
16047
16074
  }
16048
16075
  if (operations.has('UPDATE')) {
@@ -16050,6 +16077,7 @@ class SurfaceOpenMaterializerService {
16050
16077
  id: 'edit',
16051
16078
  action: 'edit',
16052
16079
  label: `Editar ${noun}`,
16080
+ tooltip: this.resolveRelatedActionDescription(payload, 'edit', noun),
16053
16081
  formId: `${formIdPrefix}.edit`,
16054
16082
  icon: 'edit',
16055
16083
  color: 'primary',
@@ -16060,14 +16088,15 @@ class SurfaceOpenMaterializerService {
16060
16088
  cardinality: { min: 1, max: 1 },
16061
16089
  },
16062
16090
  openMode: 'drawer',
16063
- form: this.buildExplicitRelatedActionForm('edit', paths),
16091
+ form: this.buildExplicitRelatedActionForm('edit', paths, undefined, contextFields),
16064
16092
  });
16065
16093
  }
16066
16094
  if (operations.has('DELETE')) {
16067
16095
  actions.push({
16068
16096
  id: 'delete',
16069
16097
  action: 'delete',
16070
- label: `Apagar ${noun}`,
16098
+ label: `Remover ${noun}`,
16099
+ tooltip: this.resolveRelatedActionDescription(payload, 'delete', noun),
16071
16100
  formId: `${formIdPrefix}.delete`,
16072
16101
  icon: 'delete',
16073
16102
  color: 'warn',
@@ -16109,7 +16138,7 @@ class SurfaceOpenMaterializerService {
16109
16138
  },
16110
16139
  };
16111
16140
  }
16112
- buildExplicitRelatedActionForm(action, paths) {
16141
+ buildExplicitRelatedActionForm(action, paths, initialValue, contextFields) {
16113
16142
  const resourcePath = this.normalizeResourcePath(paths?.resourcePath || '');
16114
16143
  const schemaResourcePath = this.normalizeResourcePath(paths?.schemaResourcePath || resourcePath);
16115
16144
  if (!resourcePath) {
@@ -16120,7 +16149,10 @@ class SurfaceOpenMaterializerService {
16120
16149
  schemaUrl: this.buildSchemaUrl(schemaResourcePath, 'post', 'request'),
16121
16150
  submitUrl: resourcePath,
16122
16151
  submitMethod: 'post',
16123
- layoutPolicy: this.buildRelatedCommandLayoutPolicy('create'),
16152
+ ...(initialValue && Object.keys(initialValue).length
16153
+ ? { initialValue }
16154
+ : {}),
16155
+ layoutPolicy: this.buildRelatedCommandLayoutPolicy('create', contextFields),
16124
16156
  };
16125
16157
  }
16126
16158
  const itemSchemaPath = schemaResourcePath ? `${schemaResourcePath}/{id}` : '';
@@ -16130,7 +16162,7 @@ class SurfaceOpenMaterializerService {
16130
16162
  schemaUrl: this.buildSchemaUrl(itemSchemaPath, 'put', 'request'),
16131
16163
  submitUrl: itemSubmitUrl,
16132
16164
  submitMethod: 'put',
16133
- layoutPolicy: this.buildRelatedCommandLayoutPolicy('update'),
16165
+ layoutPolicy: this.buildRelatedCommandLayoutPolicy('update', contextFields),
16134
16166
  };
16135
16167
  }
16136
16168
  return {
@@ -16138,7 +16170,7 @@ class SurfaceOpenMaterializerService {
16138
16170
  submitMethod: 'delete',
16139
16171
  };
16140
16172
  }
16141
- buildRelatedCommandLayoutPolicy(schemaOperation) {
16173
+ buildRelatedCommandLayoutPolicy(schemaOperation, contextFields) {
16142
16174
  return {
16143
16175
  source: 'schema',
16144
16176
  intent: 'command',
@@ -16147,6 +16179,9 @@ class SurfaceOpenMaterializerService {
16147
16179
  persistence: 'transient',
16148
16180
  schemaOperation,
16149
16181
  schemaType: 'request',
16182
+ ...(contextFields?.length
16183
+ ? { groupedCommand: { contextFields } }
16184
+ : {}),
16150
16185
  };
16151
16186
  }
16152
16187
  buildSchemaUrl(path, operation, schemaType) {
@@ -16178,6 +16213,21 @@ class SurfaceOpenMaterializerService {
16178
16213
  const relatedResource = payload.context?.['relatedResource'];
16179
16214
  return relatedResource;
16180
16215
  }
16216
+ buildRelatedParentInitialValue(payload) {
16217
+ const parentField = String(this.resolveRelatedResource(payload)?.childParentField || '').trim();
16218
+ const resource = payload.context?.['resource'];
16219
+ const parentResourceId = resource?.['resourceId'];
16220
+ if (!parentField ||
16221
+ parentResourceId == null ||
16222
+ String(parentResourceId).trim() === '') {
16223
+ return undefined;
16224
+ }
16225
+ return { [parentField]: parentResourceId };
16226
+ }
16227
+ buildRelatedContextFields(payload) {
16228
+ const parentField = String(this.resolveRelatedResource(payload)?.childParentField || '').trim();
16229
+ return parentField ? [parentField] : [];
16230
+ }
16181
16231
  resolveRelatedSelectionKeyField(payload) {
16182
16232
  const relatedResource = this.resolveRelatedResource(payload);
16183
16233
  const field = String(relatedResource?.selectionKeyField || '').trim();
@@ -16214,22 +16264,42 @@ class SurfaceOpenMaterializerService {
16214
16264
  ]
16215
16265
  .map((value) => String(value || '').trim())
16216
16266
  .find(Boolean) || '';
16217
- const normalized = source
16218
- .normalize('NFD')
16219
- .replace(/[\u0300-\u036f]/g, '')
16220
- .toLowerCase();
16221
- if (/\b(documentos?|documents?)\b/.test(normalized)) {
16222
- return 'documento';
16223
- }
16224
- const firstToken = normalized
16225
- .split(/[^a-z0-9]+/)
16267
+ const firstToken = source
16268
+ .split(/[^\p{L}\p{N}]+/u)
16226
16269
  .filter(Boolean)
16227
- .find((token) => !['de', 'da', 'do', 'das', 'dos', 'legal', 'legais'].includes(token));
16228
- if (firstToken && firstToken.length > 3) {
16229
- return firstToken.endsWith('s') ? firstToken.slice(0, -1) : firstToken;
16270
+ .find((token) => !['de', 'da', 'do', 'das', 'dos'].includes(token.toLocaleLowerCase('pt-BR')));
16271
+ if (firstToken && firstToken.length > 2) {
16272
+ return this.singularizePtBrWord(firstToken).toLocaleLowerCase('pt-BR');
16230
16273
  }
16231
16274
  return 'item';
16232
16275
  }
16276
+ singularizePtBrWord(word) {
16277
+ if (/ções$/iu.test(word))
16278
+ return word.replace(/ções$/iu, 'ção');
16279
+ if (/ões$/iu.test(word))
16280
+ return word.replace(/ões$/iu, 'ão');
16281
+ if (/ais$/iu.test(word))
16282
+ return word.replace(/ais$/iu, 'al');
16283
+ if (/eis$/iu.test(word))
16284
+ return word.replace(/eis$/iu, 'el');
16285
+ if (/res$/iu.test(word))
16286
+ return word.replace(/es$/iu, '');
16287
+ if (/s$/iu.test(word) && !/ss$/iu.test(word))
16288
+ return word.replace(/s$/iu, '');
16289
+ return word;
16290
+ }
16291
+ resolveRelatedActionDescription(payload, action, noun) {
16292
+ const surfaceDescription = String(payload.subtitle
16293
+ || payload.context?.['surface']?.['description']
16294
+ || '').trim();
16295
+ if (surfaceDescription)
16296
+ return surfaceDescription;
16297
+ if (action === 'create')
16298
+ return `Adicione ${noun} ao contexto selecionado.`;
16299
+ if (action === 'edit')
16300
+ return `Atualize os dados de ${noun} no contexto selecionado.`;
16301
+ return `Remova ${noun} do contexto selecionado.`;
16302
+ }
16233
16303
  inferColumnsFromData(data) {
16234
16304
  const first = data.find((item) => item && typeof item === 'object' && !Array.isArray(item));
16235
16305
  if (!first || typeof first !== 'object')
@@ -23415,6 +23485,7 @@ function normalizeLayoutPolicy(policy) {
23415
23485
  ? {
23416
23486
  orphanFieldExpansion: policy?.groupedCommand?.orphanFieldExpansion ??
23417
23487
  'medium-and-wide',
23488
+ contextFields: normalizeFieldNameList(policy?.groupedCommand?.contextFields),
23418
23489
  }
23419
23490
  : policy?.groupedCommand,
23420
23491
  };
@@ -23692,6 +23763,12 @@ function spanForColumns(columns) {
23692
23763
  return Math.max(1, Math.min(12, Math.floor(12 / columns)));
23693
23764
  }
23694
23765
  function fieldsForPolicy(fields, policy) {
23766
+ if (policy.preset === 'groupedCommand') {
23767
+ const contextFields = groupedCommandContextFields(policy);
23768
+ return fields.map((field) => contextFields.has(field.name)
23769
+ ? { ...field, formHidden: true, hidden: true }
23770
+ : field);
23771
+ }
23695
23772
  if (policy.preset !== 'compactPresentation') {
23696
23773
  return fields;
23697
23774
  }
@@ -23710,11 +23787,23 @@ function fieldsForPolicy(fields, policy) {
23710
23787
  }));
23711
23788
  }
23712
23789
  function fieldsForLayoutPolicy(fields, policy) {
23790
+ if (policy.preset === 'groupedCommand') {
23791
+ const contextFields = groupedCommandContextFields(policy);
23792
+ return fields.filter((field) => !contextFields.has(field.name));
23793
+ }
23713
23794
  if (policy.preset !== 'compactPresentation') {
23714
23795
  return fields;
23715
23796
  }
23716
23797
  return resolveDetailSummaryFields(fields, policy);
23717
23798
  }
23799
+ function groupedCommandContextFields(policy) {
23800
+ return new Set(normalizeFieldNameList(policy.groupedCommand?.contextFields));
23801
+ }
23802
+ function normalizeFieldNameList(value) {
23803
+ return Array.from(new Set((value || [])
23804
+ .map((fieldName) => String(fieldName || '').trim())
23805
+ .filter(Boolean)));
23806
+ }
23718
23807
  function resolvePresentationFields(fields) {
23719
23808
  return fields.filter((field) => !isFormHidden(field));
23720
23809
  }