Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Trees**: The GetFeatureInfo filter is labeled as the feature-information filter, and the GetMap filter is labeled as the map filter ([#491](https://github.com/sitmun/sitmun-admin-app/issues/491)).
- **Trees**: Accepting a name or description translation on a saved node marks the form dirty and sends the node update ([#504](https://github.com/sitmun/sitmun-admin-app/issues/504)).
- **Trees**: Loading layers from a service attaches the cartography of that service when two services share a layer name ([#227](https://github.com/sitmun/sitmun-admin-app/issues/227)).
- **More Info Advanced**: A child task mapping stores the query parameter name. The option text stays the label ([#506](https://github.com/sitmun/sitmun-admin-app/issues/506)).
- **Services**: GetCapabilities trims surrounding spaces from the service URL and keeps the trimmed value on the form ([#233](https://github.com/sitmun/sitmun-admin-app/issues/233)).
- **Layers**: On a WFS cartography, the development-only styles, filters, feature-information, and legend copy says feature type. WMS copy is unchanged ([#492](https://github.com/sitmun/sitmun-admin-app/issues/492)).
- **Languages**: Set as Default no longer stays disabled after open. Default-language lookups use a single `/api/` slash so Spring Security 7 accepts `findByName`.
- **Grids**: AG Grid 32 registers `CommunityFeaturesModule` so text/select cell editors mount. Date columns use the Instant-safe `datePicker` editor with `cellDataType: false` so Positions Alta/Baja edit no longer throws in `DateStringCellEditor`.
- **Tests**: Jest specs match Angular 22 realities (`ng-reflect-*` opt-in, HostListener rename, `@if (; as)` syntax, jsdom Location, FileReader under Node 24) and use a 30s default timeout.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@
<mat-card appearance="outlined" class="sitmun-mat-card-margin">
<mat-card-content class="sitmun-service-form-stack">
<mat-slide-toggle color="primary" formControlName="queryableFeatureAvailable">
{{ 'entity.cartography.featureInformation.layerAll' | translate }}
{{ cartographyCopyKey('entity.cartography.featureInformation.layerAll') | translate }}
</mat-slide-toggle>
@if (entityForm.get('queryableFeatureAvailable').value !== true) {
<mat-form-field appearance="outline" class="half add-gap"
Expand Down Expand Up @@ -362,7 +362,7 @@
<mat-card appearance="outlined" class="sitmun-mat-card-margin">
<mat-card-content class="sitmun-service-form-stack">
<mat-slide-toggle color="primary" formControlName="applyFilterToGetMap">
{{ 'entity.cartography.filters.applyFilterToGetMap' | translate }}
{{ cartographyCopyKey('entity.cartography.filters.applyFilterToGetMap') | translate }}
</mat-slide-toggle>
<mat-slide-toggle color="primary" formControlName="applyFilterToSpatialSelection">
{{ 'entity.cartography.filters.applyFilterToSpatialSelection' | translate }}
Expand Down Expand Up @@ -399,7 +399,7 @@
<mat-icon aria-hidden="true">info</mat-icon>
<div>
<p>{{ 'entity.cartography.experimentalSitnaProfileNotice' | translate }}</p>
<p class="sitmun-experimental-notice-secondary">{{ 'entity.cartography.legend.sitnaControlCapabilities' | translate }}</p>
<p class="sitmun-experimental-notice-secondary">{{ cartographyCopyKey('entity.cartography.legend.sitnaControlCapabilities') | translate }}</p>
</div>
</mat-card-content>
</mat-card>
Expand Down Expand Up @@ -452,7 +452,7 @@
<mat-card-content class="sitmun-service-form-stack">
<div class="sitmun-toggle-with-hint">
<mat-slide-toggle color="primary" formControlName="useAllStyles">
{{ 'entity.cartography.styles.useAllStyles' | translate }}
{{ cartographyCopyKey('entity.cartography.styles.useAllStyles') | translate }}
</mat-slide-toggle>
<p class="sitmun-form-hint-text">{{ 'entity.cartography.styles.useAllStylesHint' | translate }}</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,38 @@ describe('LayersFormComponent', () => {
expect(territoriesTab).not.toContain('sitmun-cartography-form-entity');
});

it('uses feature-type wording when the linked service is WFS', () => {
const flags = TestBed.inject(FeatureFlagService);
expect(flags.isFeatureEnabled('LAYERS_LEGEND_TAB_FEATURE')).toBe(true);
expect(flags.isFeatureEnabled('LAYERS_CARTOGRAPHY_STYLES_TAB_FEATURE')).toBe(true);
expect(flags.isFeatureEnabled('LAYERS_FILTERS_TAB_FEATURE')).toBe(true);
expect(flags.isFeatureEnabled('LAYERS_FEATURE_INFORMATION_TAB_FEATURE')).toBe(true);

component.services = [{ id: 7, type: 'WFS' }];
component.entityForm.patchValue({ serviceId: 7 });

expect(component.cartographyCopyKey('entity.cartography.styles.useAllStyles'))
.toBe('entity.cartography.styles.useAllStyles.wfs');
expect(component.cartographyCopyKey('entity.cartography.filters.applyFilterToGetMap'))
.toBe('entity.cartography.filters.applyFilterToGetMap.wfs');
expect(component.cartographyCopyKey('entity.cartography.featureInformation.layerAll'))
.toBe('entity.cartography.featureInformation.layerAll.wfs');
expect(component.cartographyCopyKey('entity.cartography.legend.sitnaControlCapabilities'))
.toBe('entity.cartography.legend.sitnaControlCapabilities.wfs');
expect(layersFormTemplate).toContain("cartographyCopyKey('entity.cartography.styles.useAllStyles')");
expect(layersFormTemplate).toContain("cartographyCopyKey('entity.cartography.filters.applyFilterToGetMap')");
expect(layersFormTemplate).toContain("cartographyCopyKey('entity.cartography.featureInformation.layerAll')");
expect(layersFormTemplate).toContain("cartographyCopyKey('entity.cartography.legend.sitnaControlCapabilities')");
});

it('keeps layer wording when the linked service is WMS', () => {
component.services = [{ id: 7, type: 'WMS' }];
component.entityForm.patchValue({ serviceId: 7 });

expect(component.cartographyCopyKey('entity.cartography.styles.useAllStyles'))
.toBe('entity.cartography.styles.useAllStyles');
});

it('wraps Permissions and Trees relation grids in matTabContent', () => {
const permissionsTab = layersFormTemplate.match(
/entity\.cartography\.permissions\.header[\s\S]*?<\/mat-tab>/,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,11 @@ export class LayersFormComponent extends BaseFormComponent<CartographyProjection
return protocol == null ? null : `entity.cartography.serviceParameters.${audience}.${protocol}`;
}

cartographyCopyKey(key: string): string {
const service = this.services.find((item) => item.id === this.entityForm?.get('serviceId')?.value);
return service?.type === 'WFS' ? `${key}.wfs` : key;
}

serviceIsProxied(): boolean {
const serviceId = this.entityForm?.get('serviceId')?.value;
return this.services.find((service) => service.id === serviceId)?.isProxied === true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1060,5 +1060,18 @@ describe('ServiceFormComponent', () => {
expect(component.loneDetail()).toBe(false);
});
});

it('trims surrounding spaces from the service URL before GetCapabilities', () => {
component.entityForm.patchValue({
serviceURL: ' https://example.test/wms ',
type: 'WMS',
authenticationMode: 'none',
});

const probe = component['capabilitiesProbe']();

expect(probe.url).toBe('https://example.test/wms');
expect(component.entityForm.get('serviceURL')?.value).toBe('https://example.test/wms');
});
});

Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,12 @@ export class ServiceFormComponent extends BaseFormComponent<Service> implements

private capabilitiesProbe(): ServiceCapabilitiesProbe {
const raw = this.entityForm.getRawValue();
const url = typeof raw.serviceURL === 'string' ? raw.serviceURL.trim() : raw.serviceURL;
if (url !== raw.serviceURL) {
this.entityForm.get('serviceURL')!.setValue(url);
}
const probe: ServiceCapabilitiesProbe = {
url: raw.serviceURL,
url,
type: raw.type,
authenticationMode: raw.authenticationMode,
user: raw.user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ <h3 class="included-tasks-title">{{ 'entity.task.moreInfoAdvanced.includedTasks'
@for (p of taskView.childParameters; track p) {
<mat-option
[attr.data-testid]="row.ids.option('child', p.label)"
[value]="p.label">
[value]="p.name">
{{ p.label }}
</mat-option>
}
Expand Down Expand Up @@ -346,7 +346,7 @@ <h3 class="included-tasks-title">{{ 'entity.task.moreInfoAdvanced.includedTasks'
@for (p of node.childParameters; track p) {
<mat-option
[attr.data-testid]="row.ids.option('child', p.label)"
[value]="p.label">
[value]="p.name">
{{ p.label }}
</mat-option>
}
Expand Down Expand Up @@ -406,7 +406,7 @@ <h3 class="included-tasks-title">{{ 'entity.task.moreInfoAdvanced.includedTasks'
@for (p of node.childParameters; track p) {
<mat-option
[attr.data-testid]="row.ids.option('child', p.label)"
[value]="p.label">
[value]="p.name">
{{ p.label }}
</mat-option>
}
Expand Down Expand Up @@ -461,7 +461,7 @@ <h3 class="included-tasks-title">{{ 'entity.task.moreInfoAdvanced.includedTasks'
@for (p of node.childParameters; track p) {
<mat-option
[attr.data-testid]="row.ids.option('child', p.label)"
[value]="p.label">
[value]="p.name">
{{ p.label }}
</mat-option>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,44 @@ describe('TaskMoreInfoAdvancedFormComponent', () => {
});
});

it('saves the query parameter name when the label differs', () => {
const namedQuery = {
id: 501,
name: 'Named query',
typeId: 5,
typeName: 'Consulta',
properties: {
parameters: [{name: 'code', label: 'Layer code', value: 'ignored'}]
}
};
component.entityToEdit = {
id: 100,
name: 'Advanced info',
groupId: 1,
cartographyId: 10,
properties: {
parameters: [{label: 'featureCode', value: 'feature_code'}],
childTaskOrderIds: [namedQuery.id]
}
} as any;
(component as any).cartographies = [{id: 10, name: 'Base map'}];
(component as any).allCandidateTasks = [namedQuery];
(component as any).templateChildTasks = new Map();
component.postFetchData();

const option = component.getChildParameters(namedQuery as any)[0];
(component as any).childTaskParameterMappings.set(namedQuery.id, [
{miaParam: 'featureCode', childParam: option.name}
]);

const task = component.createObject(100) as any;

expect(option.label).toBe('Layer code');
expect(task.properties.childTaskParameters).toEqual({
'501': {code: 'feature_code'}
});
});

it('deserializes nested template child parameter mappings on edit', () => {
setupForm({
childTaskOrderIds: [templateTask.id],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1084,15 +1084,19 @@ export class TaskMoreInfoAdvancedFormComponent extends BaseFormComponent<TaskPro
return null;
}
const parameter = raw as Record<string, unknown>;
const explicitName = typeof parameter['name'] === 'string' && parameter['name'].length > 0
? parameter['name']
: null;
const label = typeof parameter['label'] === 'string' && parameter['label'].length > 0
? parameter['label']
: (typeof parameter['name'] === 'string' && parameter['name'].length > 0 ? parameter['name'] : null);
: explicitName;
if (!label) {
return null;
}
return {
...(parameter as unknown as Partial<TaskMoreInfoParameter>),
label
label,
name: explicitName ?? label,
} as TaskMoreInfoParameter;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
@if (currentNodeCartography?.applyFilterToGetFeatureInfo) {
<mat-form-field appearance="outline" class="form-field-flex-1"
>
<mat-label>{{ 'entity.layer.applyFilterToGetMap' | translate }}</mat-label>
<mat-label>{{ 'entity.layer.filterInfoByMunicipality' | translate }}</mat-label>
<mat-select formControlName="filterGetFeatureInfo">
@for (option of filterOptions; track option) {
<mat-option [value]="option.value">
Expand All @@ -257,7 +257,7 @@
@if (currentNodeCartography?.applyFilterToGetMap) {
<mat-form-field appearance="outline" class="form-field-flex-1"
>
<mat-label>{{ 'entity.layer.filterInfoByMunicipality' | translate }}</mat-label>
<mat-label>{{ 'entity.layer.applyFilterToGetMap' | translate }}</mat-label>
<mat-select formControlName="filterGetMap">
@for (option of filterOptions; track option) {
<mat-option [value]="option.value">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2510,4 +2510,92 @@ describe('TreeNodesComponent', () => {
});
});

describe('non-default translation accept', () => {
function persistedLeaf(): void {
component['currentNodeId'] = 8;
component['newElement'] = false;
component.currentNodeType = constants.treeDomainKey.cartography;
component.treeNodeForm.patchValue({
id: 8,
name: 'Leaf',
description: 'Desc',
nodeType: constants.treeDomainKey.cartography,
});
component.treeNodeForm.markAsPristine();
}

it('sends a node update when a non-default name translation is accepted', async () => {
persistedLeaf();
component.nameTranslations.set(8, new Map());
const accepted = new Map([['es', { name: 'Nombre' }]]);
jest.spyOn(component['utils'], 'openTranslationDialog').mockResolvedValue({
event: 'Accept',
data: accepted,
});
const updates: any[] = [];
component.sendNodeUpdated.subscribe((node) => updates.push(node));

await component.onTreeNodeNameTranslationButtonClicked();

expect(updates).toHaveLength(1);
expect(updates[0].nameTranslationsModified).toBe(true);
expect(updates[0].status).toBe('Modified');
});

it('sends a node update when a non-default description translation is accepted', async () => {
persistedLeaf();
component.descriptionTranslations.set(8, new Map());
const accepted = new Map([['es', { description: 'Descripcion' }]]);
jest.spyOn(component['utils'], 'openTranslationDialog').mockResolvedValue({
event: 'Accept',
data: accepted,
});
const updates: any[] = [];
component.sendNodeUpdated.subscribe((node) => updates.push(node));

await component.onTreeNodeDescriptionTranslationButtonClicked();

expect(updates).toHaveLength(1);
expect(updates[0].descriptionTranslationsModified).toBe(true);
expect(updates[0].status).toBe('Modified');
});
});

describe('load from service cartography match', () => {
it('matches a repeated capabilities name to the cartography of the selected service', () => {
component.layersList = [
{ id: 1, name: 'Other service', layers: ['AD.Address'], serviceId: 10 },
{ id: 2, name: 'Selected service', layers: ['AD.Address'], serviceId: 20 },
];

const node = component['createNewNodeWithCapabilities']({ Name: 'AD.Address', Title: 'Address' }, 20);

expect(node.cartography.id).toBe(2);
expect(node.cartographyName).toBe('Selected service');
});
});

describe('filter select labels', () => {
function labelFor(controlName: string): string {
const select = fixture.nativeElement.querySelector(
`mat-select[formcontrolname="${controlName}"]`
);
return select?.closest('mat-form-field')?.querySelector('mat-label')?.textContent?.trim() ?? '';
}

it('labels GetFeatureInfo as municipality information and GetMap as map requests', () => {
component.currentNodeCartography = {
applyFilterToGetFeatureInfo: true,
applyFilterToGetMap: true,
};
setNodeContext(constants.codeValue.treeType.cartography, constants.treeDomainKey.cartography, {
nodeId: 1,
detectChanges: true,
});

expect(labelFor('filterGetFeatureInfo')).toBe('entity.layer.filterInfoByMunicipality');
expect(labelFor('filterGetMap')).toBe('entity.layer.applyFilterToGetMap');
});
});

});
Loading
Loading