[APPS-2108] date-fns adapter for datetime pickers, many datetime parsing and validation fixes (#8992)

* migrate cloud date widget to date-fns, fix test bugs

* [ci:force] update docs

* [ci:force] remove commented out code

* [APPS-2232] date cell validator, unit tests

* improved moment adapter, code cleanup

* datetime adapter, many code fixes

* code review fixes

* code cleanup

* cleanup

* fix max datetime validation, update tests

* remove e2e already covered by unit tests

* fix search date range

* remove fake demo shell e2e for search

* remove fake demo shell e2e for search page

* cleanup e2e

* migrate dynamic table to date-fns

* fix e2e formatting

* migrate protractor to unit tests

* cleanup e2e
This commit is contained in:
Denys Vuika 2023-10-15 15:58:22 +01:00 committed by GitHub
parent c637f3eb2a
commit 2f36da5765
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 881 additions and 1059 deletions

View File

@ -22,7 +22,8 @@ import {
LoginPage, LoginPage,
TasksService, TasksService,
IdentityService, IdentityService,
GroupIdentityService GroupIdentityService,
EditTaskFilterDialogPage
} from '@alfresco/adf-testing'; } from '@alfresco/adf-testing';
import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page';
import { TasksCloudDemoPage } from './../pages/tasks-cloud-demo.page'; import { TasksCloudDemoPage } from './../pages/tasks-cloud-demo.page';
@ -38,6 +39,7 @@ describe('Edit task filters cloud', () => {
const tasksCloudDemoPage = new TasksCloudDemoPage(); const tasksCloudDemoPage = new TasksCloudDemoPage();
const editTaskFilter = tasksCloudDemoPage.editTaskFilterCloud; const editTaskFilter = tasksCloudDemoPage.editTaskFilterCloud;
const taskFilter = tasksCloudDemoPage.taskFilterCloudComponent; const taskFilter = tasksCloudDemoPage.taskFilterCloudComponent;
const editTaskFilterDialog = new EditTaskFilterDialogPage();
const apiService = createApiService(); const apiService = createApiService();
const identityService = new IdentityService(apiService); const identityService = new IdentityService(apiService);
@ -49,10 +51,18 @@ describe('Edit task filters cloud', () => {
const completedTaskName = StringUtil.generateRandomString(); const completedTaskName = StringUtil.generateRandomString();
const assignedTaskName = StringUtil.generateRandomString(); const assignedTaskName = StringUtil.generateRandomString();
/**
* Click on the specified task filter
*
* @param name filter name
*/
async function clickTaskFilter(name: string) { async function clickTaskFilter(name: string) {
await taskFilter.clickTaskFilter(name); await taskFilter.clickTaskFilter(name);
} }
/**
* Wait till the datatable component is loaded
*/
async function waitTillContentLoaded() { async function waitTillContentLoaded() {
await tasksCloudDemoPage.taskListCloudComponent().getDataTable().waitTillContentLoaded(); await tasksCloudDemoPage.taskListCloudComponent().getDataTable().waitTillContentLoaded();
} }
@ -187,7 +197,6 @@ describe('Edit task filters cloud', () => {
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
const editTaskFilterDialog = editTaskFilter.editTaskFilterDialog();
await editTaskFilterDialog.setFilterName('New'); await editTaskFilterDialog.setFilterName('New');
await editTaskFilterDialog.clickOnSaveButton(); await editTaskFilterDialog.clickOnSaveButton();
@ -197,8 +206,8 @@ describe('Edit task filters cloud', () => {
await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('id'); await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('id');
await editTaskFilter.setSortFilterDropDown('priority'); await editTaskFilter.setSortFilterDropDown('priority');
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
await editTaskFilter.editTaskFilterDialog().setFilterName('New'); await editTaskFilterDialog.setFilterName('New');
await editTaskFilter.editTaskFilterDialog().clickOnSaveButton(); await editTaskFilterDialog.clickOnSaveButton();
await expect(await taskFilter.getActiveFilterName()).toBe('New'); await expect(await taskFilter.getActiveFilterName()).toBe('New');
await editTaskFilter.openFilter(); await editTaskFilter.openFilter();
@ -224,7 +233,6 @@ describe('Edit task filters cloud', () => {
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
const editTaskFilterDialog = await editTaskFilter.editTaskFilterDialog();
await editTaskFilterDialog.setFilterName('New'); await editTaskFilterDialog.setFilterName('New');
await editTaskFilterDialog.clickOnSaveButton(); await editTaskFilterDialog.clickOnSaveButton();
@ -252,7 +260,6 @@ describe('Edit task filters cloud', () => {
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
const editTaskFilterDialog = await editTaskFilter.editTaskFilterDialog();
await editTaskFilterDialog.setFilterName('New'); await editTaskFilterDialog.setFilterName('New');
await editTaskFilterDialog.clickOnSaveButton(); await editTaskFilterDialog.clickOnSaveButton();
@ -275,12 +282,10 @@ describe('Edit task filters cloud', () => {
await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('priority'); await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('priority');
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
const dialog = editTaskFilter.editTaskFilterDialog(); await expect(await editTaskFilterDialog.getFilterName()).toEqual('My Tasks');
await editTaskFilterDialog.setFilterName('Cancel');
await expect(await dialog.getFilterName()).toEqual('My Tasks'); await expect(await editTaskFilterDialog.getFilterName()).toEqual('Cancel');
await dialog.setFilterName('Cancel'); await editTaskFilterDialog.clickOnCancelButton();
await expect(await dialog.getFilterName()).toEqual('Cancel');
await dialog.clickOnCancelButton();
await taskFilter.checkTaskFilterNotDisplayed('Cancel'); await taskFilter.checkTaskFilterNotDisplayed('Cancel');
await expect(await taskFilter.getActiveFilterName()).toEqual('My Tasks'); await expect(await taskFilter.getActiveFilterName()).toEqual('My Tasks');
@ -307,14 +312,12 @@ describe('Edit task filters cloud', () => {
await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('id'); await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('id');
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
const dialog = editTaskFilter.editTaskFilterDialog(); await expect(await editTaskFilterDialog.getFilterName()).toEqual('My Tasks');
await editTaskFilterDialog.clearFilterName();
await expect(await dialog.getFilterName()).toEqual('My Tasks'); await expect(await editTaskFilterDialog.getFilterName()).toEqual('');
await dialog.clearFilterName(); await expect(await editTaskFilterDialog.checkSaveButtonIsEnabled()).toEqual(false);
await expect(await dialog.getFilterName()).toEqual(''); await expect(await editTaskFilterDialog.checkCancelButtonIsEnabled()).toEqual(true);
await expect(await dialog.checkSaveButtonIsEnabled()).toEqual(false); await editTaskFilterDialog.clickOnCancelButton();
await expect(await dialog.checkCancelButtonIsEnabled()).toEqual(true);
await dialog.clickOnCancelButton();
}); });
it('[C291799] Task filter dialog is displayed when clicking on Save As button', async () => { it('[C291799] Task filter dialog is displayed when clicking on Save As button', async () => {
@ -326,15 +329,18 @@ describe('Edit task filters cloud', () => {
await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('id'); await expect(await editTaskFilter.getSortFilterDropDownValue()).toEqual('id');
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
const dialog = editTaskFilter.editTaskFilterDialog(); await expect(await editTaskFilterDialog.checkSaveButtonIsEnabled()).toEqual(true);
await expect(await editTaskFilterDialog.checkCancelButtonIsEnabled()).toEqual(true);
await expect(await dialog.checkSaveButtonIsEnabled()).toEqual(true); await expect(await editTaskFilterDialog.getTitle()).toEqual('Save filter as');
await expect(await dialog.checkCancelButtonIsEnabled()).toEqual(true); await expect(await editTaskFilterDialog.getFilterName()).toEqual('My Tasks');
await expect(await dialog.getTitle()).toEqual('Save filter as'); await editTaskFilterDialog.clickOnCancelButton();
await expect(await dialog.getFilterName()).toEqual('My Tasks');
await dialog.clickOnCancelButton();
}); });
/**
* Creates new custom filter
*
* @param name Filter name
*/
async function createNewCustomFilter(name: string): Promise<void> { async function createNewCustomFilter(name: string): Promise<void> {
await clickTaskFilter('my-tasks'); await clickTaskFilter('my-tasks');
await waitTillContentLoaded(); await waitTillContentLoaded();
@ -347,8 +353,7 @@ describe('Edit task filters cloud', () => {
await editTaskFilter.clickSaveAsButton(); await editTaskFilter.clickSaveAsButton();
const dialog = editTaskFilter.editTaskFilterDialog(); await editTaskFilterDialog.setFilterName(name);
await dialog.setFilterName(name); await editTaskFilterDialog.clickOnSaveButton();
await dialog.clickOnSaveButton();
} }
}); });

View File

@ -15,17 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { createApiService, ApplicationsUtil, LoginPage, ModelsActions, UsersActions, Widget, UserModel } from '@alfresco/adf-testing';
createApiService,
ApplicationsUtil,
DatePickerCalendarPage,
DateUtil,
LoginPage,
ModelsActions,
UsersActions,
Widget,
UserModel
} from '@alfresco/adf-testing';
import { ProcessFiltersPage } from '../pages/process-filters.page'; import { ProcessFiltersPage } from '../pages/process-filters.page';
import { ProcessServiceTabBarPage } from '../pages/process-service-tab-bar.page'; import { ProcessServiceTabBarPage } from '../pages/process-service-tab-bar.page';
import { NavigationBarPage } from '../../core/pages/navigation-bar.page'; import { NavigationBarPage } from '../../core/pages/navigation-bar.page';
@ -35,7 +25,6 @@ describe('Dynamic Table', () => {
const loginPage = new LoginPage(); const loginPage = new LoginPage();
const processFiltersPage = new ProcessFiltersPage(); const processFiltersPage = new ProcessFiltersPage();
const processServiceTabBarPage = new ProcessServiceTabBarPage(); const processServiceTabBarPage = new ProcessServiceTabBarPage();
const datePicker = new DatePickerCalendarPage();
const navigationBarPage = new NavigationBarPage(); const navigationBarPage = new NavigationBarPage();
const widget = new Widget(); const widget = new Widget();
@ -58,78 +47,6 @@ describe('Dynamic Table', () => {
await usersActions.deleteTenant(tenantId); await usersActions.deleteTenant(tenantId);
}); });
describe('Date Picker', () => {
const app = browser.params.resources.Files.DYNAMIC_TABLE_APP;
const randomText = {
date: '12/12/2012',
wrongDate: 'HELLO WORLD',
wrongDateTime: 'Test',
dateTime: '15/07/2019 23:55',
error: `Invalid 'columnDate' format.`,
requiredError: `Field 'columnDate' is required.`
};
const currentDate = DateUtil.formatDate('DD-MM-YYYY');
const rowPosition = 0;
beforeAll(async () => {
await apiService.login(user.username, user.password);
const applicationsService = new ApplicationsUtil(apiService);
const importedApp = await applicationsService.importPublishDeployApp(app.file_path);
appId = importedApp.id;
await loginPage.login(user.username, user.password);
});
afterAll(async () => {
await apiService.login(user.username, user.password);
await modelsActions.deleteModel(appId);
await navigationBarPage.clickLogoutButton();
});
beforeEach(async () => {
await (await (await navigationBarPage.navigateToProcessServicesPage()).goToTaskApp()).clickProcessButton();
await processServiceTabBarPage.clickProcessButton();
await processFiltersPage.clickCreateProcessButton();
await processFiltersPage.clickNewProcessDropdown();
});
it('[C286277] Should have a datepicker and a mask for DateTime field', async () => {
await widget.dynamicTable().clickAddRow();
await widget.dynamicTable().clickColumnDateTime();
await expect(await widget.dynamicTable().addRandomStringOnDateTime(randomText.wrongDateTime)).toBe('');
});
it('[C286279] Should be able to save row with Date field', async () => {
await widget.dynamicTable().clickAddRow();
await widget.dynamicTable().addRandomStringOnDate(randomText.wrongDate);
await widget.dynamicTable().clickSaveButton();
await expect(await widget.dynamicTable().checkErrorMessage()).toBe(randomText.error);
await widget.dynamicTable().clickDateWidget();
await datePicker.selectTodayDate();
await datePicker.checkDatePickerIsNotDisplayed();
await widget.dynamicTable().clickSaveButton();
await widget.dynamicTable().getTableRow(rowPosition);
await expect(await widget.dynamicTable().getTableCellText(rowPosition, 1)).toBe(currentDate);
});
it('[C311456] Should be able to delete date that is not mandatory and save the Dynamic Table', async () => {
await widget.dynamicTable().clickAddRow();
await widget.dynamicTable().clickSaveButton();
await expect(await widget.dynamicTable().checkErrorMessage()).toBe(randomText.requiredError);
await widget.dynamicTable().clickDateWidget();
await datePicker.selectTodayDate();
await datePicker.checkDatePickerIsNotDisplayed();
await widget.dynamicTable().clickSaveButton();
await widget.dynamicTable().getTableRow(rowPosition);
await expect(await widget.dynamicTable().getTableCellText(rowPosition, 1)).toBe(currentDate);
await expect(await widget.dynamicTable().getTableCellText(rowPosition, 2)).toBe('');
});
});
describe('Required Dropdown', () => { describe('Required Dropdown', () => {
const app = browser.params.resources.Files.APP_DYNAMIC_TABLE_DROPDOWN; const app = browser.params.resources.Files.APP_DYNAMIC_TABLE_DROPDOWN;
const dropdown = widget.dropdown(); const dropdown = widget.dropdown();

View File

@ -1,104 +0,0 @@
/*!
* @license
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiService,
ApplicationsUtil,
LoginPage,
ProcessUtil,
UsersActions,
Widget, UserModel
} from '@alfresco/adf-testing';
import { TasksPage } from '../pages/tasks.page';
import { browser } from 'protractor';
import CONSTANTS = require('../../util/constants');
import { ProcessServicesPage } from '../pages/process-services.page';
import { AppDefinitionRepresentation, ProcessInstanceRepresentation } from '@alfresco/js-api';
describe('Date and time widget', () => {
const app = browser.params.resources.Files.WIDGET_CHECK_APP.DATETIME;
const loginPage = new LoginPage();
const taskPage = new TasksPage();
const widget = new Widget();
const apiService = createApiService();
const usersActions = new UsersActions(apiService);
const applicationsService = new ApplicationsUtil(apiService);
const processUtil = new ProcessUtil(apiService);
let processUserModel: UserModel;
let appModel: AppDefinitionRepresentation;
let deployedAppId: number;
let process: ProcessInstanceRepresentation;
beforeAll(async () => {
await apiService.loginWithProfile('admin');
processUserModel = await usersActions.createUser();
await apiService.login(processUserModel.username, processUserModel.password);
appModel = await applicationsService.importPublishDeployApp(browser.params.resources.Files.WIDGET_CHECK_APP.file_path);
deployedAppId = await applicationsService.getAppDefinitionId(appModel.id);
process = await processUtil.startProcessByDefinitionName(appModel.name, app.processName);
await loginPage.login(processUserModel.username, processUserModel.password);
});
beforeEach(async () => {
await new ProcessServicesPage().goToAppByAppId(`${deployedAppId}`);
await taskPage.filtersPage().goToFilter(CONSTANTS.TASK_FILTERS.MY_TASKS);
await taskPage.formFields().checkFormIsDisplayed();
});
afterAll(async () => {
await processUtil.cancelProcessInstance(process.id);
await apiService.loginWithProfile('admin');
await usersActions.deleteTenant(processUserModel.tenantId);
});
it('[C268818] Should be able to set general settings for Date Time widget', async () => {
await expect(await widget.dateTimeWidget().getDateTimeLabel(app.FIELD.date_time_input)).toContain('Date');
await expect(await taskPage.formFields().isCompleteFormButtonEnabled()).toEqual(false);
await widget.dateTimeWidget().openDatepicker(app.FIELD.date_time_input);
await widget.dateTimeWidget().selectDay('10');
await widget.dateTimeWidget().selectHour('8');
await widget.dateTimeWidget().selectMinute('30');
await expect(await taskPage.formFields().isCompleteFormButtonEnabled()).toEqual(true);
await expect(await widget.dateTimeWidget().getPlaceholder(app.FIELD.date_time_between_input)).toBe('Choose anything...');
});
it('[C268819] Should be able to set advanced settings for Date Time widget ', async () => {
await widget.dateTimeWidget().setDateTimeInput(app.FIELD.date_time_between_input, '20-03-17 07:30 PM');
await widget.dateTimeWidget().closeDataTimeWidget();
await taskPage.formFields().completeForm();
await expect(await widget.dateTimeWidget().getErrorMessage(app.FIELD.date_time_between_input)).toContain('Can\'t be less than');
await browser.refresh();
await widget.dateTimeWidget().setDateTimeInput(app.FIELD.date_time_between_input, '20-03-19 07:30 PM');
await widget.dateTimeWidget().closeDataTimeWidget();
await taskPage.formFields().completeForm();
await expect(await widget.dateTimeWidget().getErrorMessage(app.FIELD.date_time_between_input)).toContain('Can\'t be greater than');
});
});

View File

@ -3,15 +3,9 @@
"C272819": "https://alfresco.atlassian.net/browse/ADF-5385", "C272819": "https://alfresco.atlassian.net/browse/ADF-5385",
"C362241": "https://alfresco.atlassian.net/browse/ADF-5385", "C362241": "https://alfresco.atlassian.net/browse/ADF-5385",
"C246534": "https://alfresco.atlassian.net/browse/ACS-4468", "C246534": "https://alfresco.atlassian.net/browse/ACS-4468",
"C268151": "https://alfresco.atlassian.net/browse/ACS-4467",
"C260377": "https://alfresco.atlassian.net/browse/ACS-4467",
"C260375": "https://alfresco.atlassian.net/browse/ACS-4467",
"C286290": "https://alfresco.atlassian.net/browse/ACS-4467",
"C286472": "https://alfresco.atlassian.net/browse/ACS-4467",
"C260387": "https://alfresco.atlassian.net/browse/ACS-4595", "C260387": "https://alfresco.atlassian.net/browse/ACS-4595",
"C216430": "https://alfresco.atlassian.net/browse/ACS-4595", "C216430": "https://alfresco.atlassian.net/browse/ACS-4595",
"C280063": "https://alfresco.atlassian.net/browse/ACS-4595", "C280063": "https://alfresco.atlassian.net/browse/ACS-4595",
"C280064": "https://alfresco.atlassian.net/browse/ACS-4595", "C280064": "https://alfresco.atlassian.net/browse/ACS-4595",
"C280407": "https://alfresco.atlassian.net/browse/ACS-4595", "C313200": "https://alfresco.atlassian.net/browse/APPS-2234"
"C277288": "https://alfresco.atlassian.net/browse/AAE-15475"
} }

View File

@ -1,226 +0,0 @@
/*!
* @license
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiService,
LocalStorageUtil,
LoginPage,
SearchSortingPickerPage,
UploadActions,
UserModel,
UsersActions
} from '@alfresco/adf-testing';
import { SearchBarPage } from '../pages/search-bar.page';
import { SearchResultsPage } from '../pages/search-results.page';
import { NavigationBarPage } from '../../core/pages/navigation-bar.page';
import { SearchFiltersPage } from '../pages/search-filters.page';
import { ContentServicesPage } from '../../core/pages/content-services.page';
import { browser } from 'protractor';
import { SearchConfiguration } from '../search.config';
import { NodesApi } from '@alfresco/js-api';
describe('Search Sorting Picker', () => {
const loginPage = new LoginPage();
const searchBarPage = new SearchBarPage();
const searchFilters = new SearchFiltersPage();
const searchResults = new SearchResultsPage();
const navigationBarPage = new NavigationBarPage();
const searchSortingPicker = new SearchSortingPickerPage();
const contentServices = new ContentServicesPage();
const acsUser = new UserModel();
const pngAModel = {
name: browser.params.resources.Files.ADF_DOCUMENTS.PNG.file_name,
location: browser.params.resources.Files.ADF_DOCUMENTS.PNG.file_path
};
const pngDModel = {
name: browser.params.resources.Files.ADF_DOCUMENTS.PNG_D.file_name,
location: browser.params.resources.Files.ADF_DOCUMENTS.PNG_D.file_path
};
let pngA; let pngD;
const apiService = createApiService();
const uploadActions = new UploadActions(apiService);
const usersActions = new UsersActions(apiService);
const nodesApi = new NodesApi(apiService.getInstance());
const search = '_png_file.png';
let jsonFile;
const checkSortingDropdownIsDisplayed = async (key = 'Modifier', label = 'Modifier') => {
await navigationBarPage.navigateToContentServices();
jsonFile = SearchConfiguration.getConfiguration();
jsonFile.sorting.options.push({
key,
label,
type: 'FIELD',
field: 'cm:modifier',
ascending: true
});
await LocalStorageUtil.setConfigField('search', JSON.stringify(jsonFile));
await searchBarPage.checkSearchIconIsVisible();
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(search);
await searchResults.dataTable.waitTillContentLoaded();
await searchSortingPicker.checkSortingDropdownIsDisplayed();
};
beforeAll(async () => {
await apiService.loginWithProfile('admin');
await usersActions.createUser(acsUser);
await apiService.login(acsUser.username, acsUser.password);
pngA = await uploadActions.uploadFile(pngAModel.location, pngAModel.name, '-my-');
pngD = await uploadActions.uploadFile(pngDModel.location, pngDModel.name, '-my-');
await browser.sleep(browser.params.testConfig.timeouts.index_search);
await loginPage.login(acsUser.username, acsUser.password);
});
afterAll(async () => {
await uploadActions.deleteFileOrFolder(pngA.entry.id);
await uploadActions.deleteFileOrFolder(pngD.entry.id);
await navigationBarPage.clickLogoutButton();
});
beforeEach(async () => {
await navigationBarPage.clickHomeButton();
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(search);
await searchResults.dataTable.waitTillContentLoaded();
});
it(`[C277269] Should see the "sort by" option when search results are displayed in search results page`, async () => {
await searchSortingPicker.checkSortingDropdownIsDisplayed();
});
it(`[C277270] Should see the icon for ASC and DESC sort when search results are displayed in the search results page`, async () => {
await searchSortingPicker.checkOrderArrowIsDisplayed();
});
it('[C277271] Should be able to add a custom search sorter in the "sort by" option', async () => {
await checkSortingDropdownIsDisplayed();
await searchSortingPicker.clickSortingDropdown();
await searchSortingPicker.checkOptionsDropdownIsDisplayed();
await searchSortingPicker.checkOptionIsDisplayed('Modifier');
});
it('[C277272] Should be able to exclude a standard search sorter from the sorting option', async () => {
await navigationBarPage.navigateToContentServices();
jsonFile = SearchConfiguration.getConfiguration();
const removedOption = jsonFile.sorting.options.splice(0, 1);
await LocalStorageUtil.setConfigField('search', JSON.stringify(jsonFile));
await searchBarPage.checkSearchIconIsVisible();
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(search);
await searchResults.dataTable.waitTillContentLoaded();
await searchSortingPicker.checkSortingDropdownIsDisplayed();
await searchSortingPicker.clickSortingDropdown();
await searchSortingPicker.checkOptionsDropdownIsDisplayed();
await searchSortingPicker.checkOptionIsNotDisplayed(removedOption[0].label);
});
it('[C277273] Should be able to set a default order for a search sorting option', async () => {
await navigationBarPage.navigateToContentServices();
jsonFile = SearchConfiguration.getConfiguration();
jsonFile.sorting.options[0].ascending = false;
jsonFile.sorting.defaults[0] = {
key: 'Size',
label: 'Size',
type: 'FIELD',
field: 'content.size',
ascending: true
};
await LocalStorageUtil.setConfigField('search', JSON.stringify(jsonFile));
await searchBarPage.checkSearchIconIsVisible();
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(search);
await searchResults.dataTable.waitTillContentLoaded();
await searchSortingPicker.checkSortingDropdownIsDisplayed();
await searchSortingPicker.clickSortingDropdown();
await searchSortingPicker.checkOptionIsDisplayed('Name');
await searchSortingPicker.clickSortingOption('Name');
await expect(await searchSortingPicker.checkOrderArrowIsDownward()).toBe(true);
});
it('[C277280] Should be able to sort the search results by "Name" ASC', async () => {
await searchFilters.checkSearchFiltersIsDisplayed();
await searchFilters.creatorCheckListFiltersPage().enterFilterInputValue(`${acsUser.firstName} ${acsUser.lastName}`);
await searchResults.sortByName('ASC');
await expect(await searchResults.checkListIsOrderedByNameAsc()).toBe(true);
});
it('[C277281] Should be able to sort the search results by "Name" DESC', async () => {
await searchFilters.checkSearchFiltersIsDisplayed();
await searchFilters.creatorCheckListFiltersPage().enterFilterInputValue(`${acsUser.firstName} ${acsUser.lastName}`);
await searchResults.sortByName('DESC');
await expect(await searchResults.checkListIsOrderedByNameDesc()).toBe(true);
});
it('[C277286] Should be able to sort the search results by "Created Date" ASC', async () => {
await searchResults.sortByCreated('ASC');
const results = await searchResults.dataTable.geCellElementDetail('Created');
await expect(contentServices.checkElementsDateSortedAsc(results)).toBe(true);
});
it('[C277287] Should be able to sort the search results by "Created Date" DESC', async () => {
await searchResults.sortByCreated('DESC');
const results = await searchResults.dataTable.geCellElementDetail('Created');
await expect(contentServices.checkElementsDateSortedDesc(results)).toBe(true);
});
it('[C277288] Should be able to sort the search results by "Modified Date" ASC', async () => {
await checkSortingDropdownIsDisplayed('Modified Date', 'Modified Date');
await searchSortingPicker.sortBy('ASC', 'Modified Date');
const idList = await contentServices.getElementsDisplayedId();
const numberOfElements = await contentServices.numberOfResultsDisplayed();
const nodeList = await getNodesDisplayed(numberOfElements, idList);
const modifiedDateList = [];
for (const item of nodeList) {
modifiedDateList.push(new Date(item.entry.modifiedAt));
}
await expect(contentServices.checkElementsDateSortedAsc(modifiedDateList)).toBe(true);
});
const getNodesDisplayed = async function(numberOfElements: number, idList: string[]) {
const promises = [];
for (let i = 0; i < (numberOfElements - 1); i++) {
if (idList[i] && idList[i].trim() !== '') {
promises.push(nodesApi.getNode(idList[i]));
}
}
return Promise.all(promises);
};
});

View File

@ -1,180 +0,0 @@
/*!
* @license
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { browser } from 'protractor';
import { createApiService, LoginPage, StringUtil, UploadActions, UserModel, UsersActions } from '@alfresco/adf-testing';
import { SearchBarPage } from './pages/search-bar.page';
import { ContentServicesPage } from '../core/pages/content-services.page';
import { SearchResultsPage } from './pages/search-results.page';
import { FolderModel } from '../models/ACS/folder.model';
import { FileModel } from '../models/ACS/file.model';
import { NavigationBarPage } from '../core/pages/navigation-bar.page';
describe('Search component - Search Page', () => {
const search = {
active: {
firstFile: null,
secondFile: null,
base: StringUtil.generateRandomString(7),
extension: '.txt'
},
no_permission: {
noPermFile: 'Meetings',
noPermFolder: 'Meeting Notes'
}
};
const loginPage = new LoginPage();
const contentServicesPage = new ContentServicesPage();
const searchBarPage = new SearchBarPage();
const searchResultPage = new SearchResultsPage();
const navigationBarPage = new NavigationBarPage();
const apiService = createApiService();
const uploadActions = new UploadActions(apiService);
const usersActions = new UsersActions(apiService);
const acsUser = new UserModel();
const emptyFolderModel = new FolderModel({ name: 'search' + StringUtil.generateRandomString() });
const newFolderModel = new FolderModel();
beforeAll(async () => {
const nrOfFiles = 15;
const adminNrOfFiles = 5;
const fileNames = StringUtil.generateFilesNames(1, nrOfFiles, search.active.base, search.active.extension);
const adminFileNames = StringUtil.generateFilesNames(nrOfFiles + 1, nrOfFiles + adminNrOfFiles, search.active.base, search.active.extension);
search.active.firstFile = fileNames[0];
search.active.secondFile = fileNames[1];
fileNames.splice(0, 1);
const firstFileModel = new FileModel({
name: search.active.firstFile,
location: browser.params.resources.Files.ADF_DOCUMENTS.TXT.file_path
});
await apiService.loginWithProfile('admin');
await usersActions.createUser(acsUser);
await apiService.login(acsUser.username, acsUser.password);
await uploadActions.createFolder(emptyFolderModel.name, '-my-');
const newFolderModelUploaded = await uploadActions.createFolder(newFolderModel.name, '-my-');
await uploadActions.createEmptyFiles(fileNames, newFolderModelUploaded.entry.id);
await uploadActions.uploadFile(firstFileModel.location, firstFileModel.name, '-my-');
await apiService.loginWithProfile('admin');
await uploadActions.createEmptyFiles(adminFileNames, newFolderModelUploaded.entry.id);
await browser.sleep(browser.params.testConfig.timeouts.index_search);
await loginPage.login(acsUser.username, acsUser.password);
});
afterAll(async () => {
await navigationBarPage.clickLogoutButton();
});
it('[C260264] Should display message when no results are found', async () => {
const notExistentFileName = StringUtil.generateRandomString();
await searchBarPage.checkSearchBarIsNotVisible();
await searchBarPage.checkSearchIconIsVisible();
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(notExistentFileName);
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkNoResultMessageIsDisplayed();
});
it('[C272810] Should display only files corresponding to search', async () => {
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(search.active.firstFile);
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkContentIsDisplayed(search.active.firstFile);
await expect(await searchResultPage.numberOfResultsDisplayed()).toBe(1);
});
it('[C260267] Should display content when opening a folder from search results', async () => {
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(emptyFolderModel.name);
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkNoResultMessageIsNotDisplayed();
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkContentIsDisplayed(emptyFolderModel.name);
await searchResultPage.navigateToFolder(emptyFolderModel.name);
const result = await contentServicesPage.currentFolderName();
await expect(result).toEqual(emptyFolderModel.name);
});
it('[C260261] Should be able to delete a file from search results', async () => {
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(search.active.firstFile);
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkContentIsDisplayed(search.active.firstFile);
await searchResultPage.deleteContent(search.active.firstFile);
await searchResultPage.checkNoResultMessageIsDisplayed();
await searchResultPage.checkContentIsNotDisplayed(search.active.firstFile);
await searchBarPage.checkSearchBarIsNotVisible();
await searchBarPage.checkSearchIconIsVisible();
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(search.active.firstFile);
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkNoResultMessageIsDisplayed();
});
it('[C272809] Should be able to delete a folder from search results', async () => {
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(emptyFolderModel.name);
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkContentIsDisplayed(emptyFolderModel.name);
await searchResultPage.checkNoResultMessageIsNotDisplayed();
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkContentIsDisplayed(emptyFolderModel.name);
await searchResultPage.deleteContent(emptyFolderModel.name);
await searchResultPage.checkNoResultMessageIsDisplayed();
await searchBarPage.checkSearchBarIsNotVisible();
await searchBarPage.checkSearchIconIsVisible();
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter(emptyFolderModel.name);
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkNoResultMessageIsDisplayed();
});
it('[C286675] Should display results when searching for all elements', async () => {
await searchBarPage.clickOnSearchIcon();
await searchBarPage.enterTextAndPressEnter('*');
await searchResultPage.dataTable.waitTillContentLoaded();
await searchResultPage.checkNoResultMessageIsNotDisplayed();
});
});

View File

@ -18,7 +18,7 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { DateAdapter } from '@angular/material/core'; import { DateAdapter } from '@angular/material/core';
import { UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core'; import { MomentDateAdapter, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { SearchWidget } from '../../models/search-widget.interface'; import { SearchWidget } from '../../models/search-widget.interface';
import { SearchWidgetSettings } from '../../models/search-widget-settings.interface'; import { SearchWidgetSettings } from '../../models/search-widget-settings.interface';
import { SearchQueryBuilderService } from '../../services/search-query-builder.service'; import { SearchQueryBuilderService } from '../../services/search-query-builder.service';
@ -26,7 +26,7 @@ import { LiveErrorStateMatcher } from '../../forms/live-error-state-matcher';
import { Moment } from 'moment'; import { Moment } from 'moment';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MomentDateAdapter } from '@angular/material-moment-adapter'; import { MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter';
export interface DateRangeValue { export interface DateRangeValue {
from: string; from: string;
@ -100,6 +100,9 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe((locale) => this.setLocale(locale)); .subscribe((locale) => this.setLocale(locale));
const customDateAdapter = this.dateAdapter as MomentDateAdapter;
customDateAdapter.overrideDisplayFormat = this.datePickerFormat;
const validators = Validators.compose([Validators.required]); const validators = Validators.compose([Validators.required]);
if (this.settings?.maxDate) { if (this.settings?.maxDate) {

View File

@ -79,7 +79,6 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
.select(UserPreferenceValues.Locale) .select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe(locale => { .subscribe(locale => {
this.dateAdapter.setLocale(locale);
this.property.locale = locale; this.property.locale = locale;
}); });

View File

@ -46,6 +46,23 @@ import { Locale } from 'date-fns';
adapter.displayFormat = '<custom date-fns format>'; adapter.displayFormat = '<custom date-fns format>';
* } * }
*/ */
/**
* Material date formats for Date-fns
*/
export const ADF_DATE_FORMATS: MatDateFormats = {
parse: {
dateInput: 'dd-MM-yyyy'
},
display: {
dateInput: 'dd-MM-yyyy',
monthLabel: 'LLL',
monthYearLabel: 'LLL uuuu',
dateA11yLabel: 'PP',
monthYearA11yLabel: 'LLLL uuuu'
}
};
@Injectable() @Injectable()
export class AdfDateFnsAdapter extends DateFnsAdapter { export class AdfDateFnsAdapter extends DateFnsAdapter {
private _displayFormat?: string = null; private _displayFormat?: string = null;

View File

@ -25,9 +25,14 @@ describe('DateFnsUtils', () => {
expect(dateFnsFormat).toBe('yyyy-MM-dd'); expect(dateFnsFormat).toBe('yyyy-MM-dd');
}); });
it('should convert moment datetime format', () => { it('should convert moment datetime format with zone', () => {
const dateFnsFormat = DateFnsUtils.convertMomentToDateFnsFormat('YYYY-MM-DDTHH:mm:ssZ'); const dateFnsFormat = DateFnsUtils.convertMomentToDateFnsFormat('YYYY-MM-DDTHH:mm:ssZ');
expect(dateFnsFormat).toBe(`yyyy-MM-dd'T'HH:mm:ss'Z'`); expect(dateFnsFormat).toBe(`yyyy-MM-dd'T'HH:mm:ssXXX`);
});
it('should convert moment datetime format with zone hours and mins', () => {
const dateFnsFormat = DateFnsUtils.convertMomentToDateFnsFormat('YYYY-MM-DDTHH:mm:ssZZ');
expect(dateFnsFormat).toBe(`yyyy-MM-dd'T'HH:mm:ssXX`);
}); });
it('should convert custom moment datetime format', () => { it('should convert custom moment datetime format', () => {
@ -81,20 +86,29 @@ describe('DateFnsUtils', () => {
expect(result).toEqual(expectedParsedDate); expect(result).toEqual(expectedParsedDate);
}); });
it('should format ISO datetime from date', () => { it('should parse alternative ISO datetime', () => {
const result = DateFnsUtils.formatDate( const result = DateFnsUtils.parseDate(
new Date('2023-10-10T18:28:50.082Z'), '1982-03-13T10:00:000Z',
`yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` `yyyy-MM-dd'T'HH:mm:sssXXX`
); );
expect(result).toBe('2023-10-10T18:28:50.082Z');
expect(result.toISOString()).toBe('1982-03-13T10:00:00.000Z');
}); });
it('should format ISO datetime from string', () => { it('should parse the datetime with zone', () => {
const result = DateFnsUtils.formatDate( const result = DateFnsUtils.parseDate(
'2023-10-10T18:28:50.082Z', '1982-03-13T10:00:000+01:00',
`yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` `yyyy-MM-dd'T'HH:mm:sssXXX`
); );
expect(result).toBe('2023-10-10T18:28:50.082Z'); expect(result.toISOString()).toBe('1982-03-13T09:00:00.000Z');
});
it('should parse datetime with zone in moment format', () => {
const result = DateFnsUtils.parseDate(
'1982-03-13T10:00:00+0100',
`YYYY-MM-DDTHH:mm:ssZZ`
);
expect(result.toISOString()).toBe('1982-03-13T09:00:00.000Z');
}); });
it('should validate datetime with moment format', () => { it('should validate datetime with moment format', () => {

View File

@ -87,7 +87,8 @@ export class DateFnsUtils {
A: 'a', A: 'a',
ll: 'PP', ll: 'PP',
T: `'T'`, T: `'T'`,
Z: `'Z'` ZZ: 'XX',
Z: `XXX`
}; };
/** /**
@ -99,9 +100,7 @@ export class DateFnsUtils {
static convertMomentToDateFnsFormat(dateDisplayFormat: string): string { static convertMomentToDateFnsFormat(dateDisplayFormat: string): string {
if (dateDisplayFormat && dateDisplayFormat.trim() !== '') { if (dateDisplayFormat && dateDisplayFormat.trim() !== '') {
// normalise the input to support double conversion of the same string // normalise the input to support double conversion of the same string
dateDisplayFormat = dateDisplayFormat dateDisplayFormat = dateDisplayFormat.replace(`'T'`, 'T');
.replace(`'T'`, 'T')
.replace(`'Z'`, 'Z');
for (const [search, replace] of Object.entries(this.momentToDateFnsMap)) { for (const [search, replace] of Object.entries(this.momentToDateFnsMap)) {
dateDisplayFormat = dateDisplayFormat.replace(new RegExp(search, 'g'), replace); dateDisplayFormat = dateDisplayFormat.replace(new RegExp(search, 'g'), replace);
@ -194,4 +193,12 @@ export class DateFnsUtils {
static isAfterDate(source: Date, target: Date): boolean { static isAfterDate(source: Date, target: Date): boolean {
return isAfter(source, target); return isAfter(source, target);
} }
static utcToLocal(date: Date): Date {
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()));
}
static localToUtc(date: Date): Date {
return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
}
} }

View File

@ -0,0 +1,141 @@
/*!
* @license
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Inject, Injectable, Optional } from '@angular/core';
import { DateFnsUtils } from './date-fns-utils';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimeFormats } from '@mat-datetimepicker/core';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { Locale, addHours, addMinutes } from 'date-fns';
/**
* Material date/time formats for Date-fns (mat-datetimepicker)
*/
export const ADF_DATETIME_FORMATS: MatDatetimeFormats = {
parse: {
dateInput: 'P', // L
monthInput: 'LLLL', // MMMM
timeInput: 'p', // LT
datetimeInput: 'Pp' // L LT
},
display: {
dateInput: 'P', // L
monthInput: 'LLLL', // MMMM
datetimeInput: 'Pp', // L LT
timeInput: 'p', // LT
monthYearLabel: 'LLL uuuu', // MMM YYYY
dateA11yLabel: 'PP', // LL
monthYearA11yLabel: 'LLLL uuuu', // MMMM YYYY
popupHeaderDateLabel: 'ccc, dd MMM' // ddd, DD MMM
}
};
/** The default hour names to use if Intl API is not available. */
const DEFAULT_HOUR_NAMES = range(24, (i) => String(i));
/** The default minute names to use if Intl API is not available. */
const DEFAULT_MINUTE_NAMES = range(60, (i) => String(i));
// eslint-disable-next-line jsdoc/require-jsdoc
function range<T>(length: number, valueFunction: (index: number) => T): T[] {
const valuesArray = Array(length);
for (let i = 0; i < length; i++) {
valuesArray[i] = valueFunction(i);
}
return valuesArray;
}
@Injectable()
export class AdfDateTimeFnsAdapter extends DatetimeAdapter<Date> {
private _displayFormat?: string = null;
get displayFormat(): string | null {
return this._displayFormat;
}
set displayFormat(value: string | null) {
this._displayFormat = value ? DateFnsUtils.convertMomentToDateFnsFormat(value) : null;
}
constructor(
@Optional() @Inject(MAT_DATE_LOCALE) matDateLocale: Locale,
@Optional() @Inject(MAT_DATETIME_FORMATS) private formats: MatDatetimeFormats,
dateAdapter: DateAdapter<Date, Locale>
) {
super(dateAdapter);
this.setLocale(matDateLocale);
}
getHour(date: Date): number {
return date.getHours();
}
getMinute(date: Date): number {
return date.getMinutes();
}
getFirstDateOfMonth(date: Date): Date {
const result = new Date();
result.setFullYear(date.getFullYear(), date.getMonth(), 1);
return result;
}
isInNextMonth(startDate: Date, endDate: Date): boolean {
const nextMonth = this.getDateInNextMonth(startDate);
return this.sameMonthAndYear(nextMonth, endDate);
}
getHourNames(): string[] {
return DEFAULT_HOUR_NAMES;
}
getMinuteNames(): string[] {
return DEFAULT_MINUTE_NAMES;
}
addCalendarHours(date: Date, hours: number): Date {
return addHours(date, hours);
}
addCalendarMinutes(date: Date, minutes: number): Date {
return addMinutes(date, minutes);
}
createDatetime(year: number, month: number, date: number, hour: number, minute: number): Date {
const result = new Date();
result.setFullYear(year, month, date);
result.setHours(hour, minute, 0, 0);
return result;
}
private getDateInNextMonth(date: Date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 1, date.getHours(), date.getMinutes());
}
override parse(value: any, parseFormat: any): Date {
return this._delegate.parse(value, parseFormat);
}
override format(date: Date, displayFormat: any): string {
displayFormat = DateFnsUtils.convertMomentToDateFnsFormat(displayFormat);
if (this.displayFormat && displayFormat === this.formats?.display?.datetimeInput) {
return this._delegate.format(date, this.displayFormat || displayFormat);
}
return this._delegate.format(date, displayFormat);
}
}

View File

@ -18,6 +18,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DateAdapter } from '@angular/material/core'; import { DateAdapter } from '@angular/material/core';
import moment, { isMoment, Moment } from 'moment'; import moment, { isMoment, Moment } from 'moment';
import { UserPreferencesService, UserPreferenceValues } from '../services/user-preferences.service';
@Injectable() @Injectable()
export class MomentDateAdapter extends DateAdapter<Moment> { export class MomentDateAdapter extends DateAdapter<Moment> {
@ -25,6 +26,14 @@ export class MomentDateAdapter extends DateAdapter<Moment> {
overrideDisplayFormat: string; overrideDisplayFormat: string;
constructor(preferences: UserPreferencesService) {
super();
preferences.select(UserPreferenceValues.Locale).subscribe((locale: string) => {
this.setLocale(locale);
});
}
getYear(date: Moment): number { getYear(date: Moment): number {
return date.year(); return date.year();
} }

View File

@ -22,3 +22,4 @@ export * from './moment-date-adapter';
export * from './string-utils'; export * from './string-utils';
export * from './date-fns-utils'; export * from './date-fns-utils';
export * from './date-fns-adapter'; export * from './date-fns-adapter';
export * from './datetime-fns-adapter';

View File

@ -65,6 +65,8 @@ import { AppConfigService } from './app-config/app-config.service';
import { StorageService } from './common/services/storage.service'; import { StorageService } from './common/services/storage.service';
import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-factories/alfresco-api-v2-loader.service'; import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-factories/alfresco-api-v2-loader.service';
import { AdfDateFnsAdapter } from './common/utils/date-fns-adapter'; import { AdfDateFnsAdapter } from './common/utils/date-fns-adapter';
import { MomentDateAdapter } from './common/utils/moment-date-adapter';
import { AdfDateTimeFnsAdapter } from './common/utils/datetime-fns-adapter';
@NgModule({ @NgModule({
imports: [ imports: [
@ -150,6 +152,8 @@ export class CoreModule {
TranslateService, TranslateService,
{ provide: TranslateLoader, useClass: TranslateLoaderService }, { provide: TranslateLoader, useClass: TranslateLoaderService },
AdfDateFnsAdapter, AdfDateFnsAdapter,
AdfDateTimeFnsAdapter,
MomentDateAdapter,
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: loadAppConfig, useFactory: loadAppConfig,

View File

@ -706,7 +706,7 @@ describe('FormFieldValidator', () => {
}); });
it('should take into account that max value is in UTC and NOT fail validating value checking the time', () => { it('should take into account that max value is in UTC and NOT fail validating value checking the time', () => {
const localValidValue = '2018-3-30 11:59 PM'; const localValidValue = '2018-03-30T22:59:00.000Z';
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
@ -718,7 +718,7 @@ describe('FormFieldValidator', () => {
}); });
it('should take into account that max value is in UTC and fail validating value checking the time', () => { it('should take into account that max value is in UTC and fail validating value checking the time', () => {
const localInvalidValue = '2018-3-31 12:01 AM'; const localInvalidValue = '2018-03-30T23:01:00.000Z';
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
@ -735,8 +735,8 @@ describe('FormFieldValidator', () => {
it('should succeed validating value checking the time', () => { it('should succeed validating value checking the time', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
value: '08-02-9999 09:10 AM', value: '9999-02-08T09:10:00.000Z',
maxValue: '9999-02-08 10:10 AM' maxValue: '9999-02-08T10:10:00.000Z'
}); });
expect(validator.validate(field)).toBeTruthy(); expect(validator.validate(field)).toBeTruthy();
@ -745,8 +745,8 @@ describe('FormFieldValidator', () => {
it('should fail validating value checking the time', () => { it('should fail validating value checking the time', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
value: '08-02-9999 11:10 AM', value: '9999-02-08T11:10:00.000Z',
maxValue: '9999-02-08 10:10 AM' maxValue: '9999-02-08T10:10:00.000Z'
}); });
field.validationSummary = new ErrorMessageModel(); field.validationSummary = new ErrorMessageModel();
@ -757,8 +757,8 @@ describe('FormFieldValidator', () => {
it('should succeed validating value checking the date', () => { it('should succeed validating value checking the date', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
value: '08-02-9999 09:10 AM', value: '9999-02-08T09:10:00.000Z',
maxValue: '9999-02-08 10:10 AM' maxValue: '9999-02-08T10:10:00.000Z'
}); });
expect(validator.validate(field)).toBeTruthy(); expect(validator.validate(field)).toBeTruthy();
@ -825,12 +825,12 @@ describe('FormFieldValidator', () => {
}); });
it('should take into account that min value is in UTC and NOT fail validating value checking the time', () => { it('should take into account that min value is in UTC and NOT fail validating value checking the time', () => {
const localValidValue = '2018-3-02 06:01 AM'; const localValidValue = '2018-03-02T06:01:00.000Z';
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
value: localValidValue, value: localValidValue,
minValue: '2018-03-02T06:00:00+00:00' minValue: '2018-03-02T06:00:00.000Z'
}); });
expect(validator.validate(field)).toBeTruthy(); expect(validator.validate(field)).toBeTruthy();
@ -874,8 +874,8 @@ describe('FormFieldValidator', () => {
it('should fail validating value by time', () => { it('should fail validating value by time', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
value: '08-02-9999 09:00 AM', value: '9999-08-02T08:10:00.000Z',
minValue: '9999-02-08 09:10 AM' minValue: '9999-08-02T08:11:00.000Z'
}); });
field.validationSummary = new ErrorMessageModel(); field.validationSummary = new ErrorMessageModel();
@ -886,8 +886,8 @@ describe('FormFieldValidator', () => {
it('should fail validating value by date', () => { it('should fail validating value by date', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
value: '07-02-9999 09:10 AM', value: '9999-02-07T09:10:00.000Z',
minValue: '9999-02-08 09:10 AM' minValue: '9999-02-08T09:10:00.000Z'
}); });
field.validationSummary = new ErrorMessageModel(); field.validationSummary = new ErrorMessageModel();
@ -1110,9 +1110,9 @@ describe('FormFieldValidator', () => {
it('should not validate dateTime format with default format', () => { it('should not validate dateTime format with default format', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
value: '2021-06-09 14:10' // 14:10 does not conform to A value: '2021-06-09 14:10 AM' // 14:10 does not conform to A
}); });
expect(field.value).toBe('2021-06-09 14:10'); expect(field.value).toBe('2021-06-09 14:10 AM');
expect(field.dateDisplayFormat).toBe('D-M-YYYY hh:mm A'); expect(field.dateDisplayFormat).toBe('D-M-YYYY hh:mm A');
expect(validator.validate(field)).toBeFalse(); expect(validator.validate(field)).toBeFalse();
}); });

View File

@ -21,6 +21,7 @@ import { FormFieldTypes } from './form-field-types';
import { isNumberValue } from './form-field-utils'; import { isNumberValue } from './form-field-utils';
import { FormFieldModel } from './form-field.model'; import { FormFieldModel } from './form-field.model';
import { DateFnsUtils } from '../../../../common/utils/date-fns-utils'; import { DateFnsUtils } from '../../../../common/utils/date-fns-utils';
import { isValid as isDateValid, isBefore, isAfter } from 'date-fns';
export interface FormFieldValidator { export interface FormFieldValidator {
@ -171,18 +172,18 @@ export class DateTimeFieldValidator implements FormFieldValidator {
FormFieldTypes.DATETIME FormFieldTypes.DATETIME
]; ];
// Validates that the input string is a valid date formatted as <dateFormat> (default D-M-YYYY)
static isValidDate(inputDate: string, dateFormat: string = 'YYYY-MM-DD HH:mm'): boolean {
return DateFnsUtils.isValidDate(inputDate, dateFormat);
}
isSupported(field: FormFieldModel): boolean { isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1; return field && this.supportedTypes.indexOf(field.type) > -1;
} }
static isValidDateTime(input: string): boolean {
const date = new Date(input);
return isDateValid(date);
}
validate(field: FormFieldModel): boolean { validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value && field.isVisible) { if (this.isSupported(field) && field.value && field.isVisible) {
if (DateFieldValidator.isValidDate(field.value, field.dateDisplayFormat)) { if (DateTimeFieldValidator.isValidDateTime(field.value)) {
return true; return true;
} }
field.validationSummary.message = field.dateDisplayFormat; field.validationSummary.message = field.dateDisplayFormat;
@ -298,24 +299,22 @@ export class MinDateTimeFieldValidator implements FormFieldValidator {
validate(field: FormFieldModel): boolean { validate(field: FormFieldModel): boolean {
let isValid = true; let isValid = true;
if (this.isSupported(field) && field.value && field.isVisible) { if (this.isSupported(field) && field.value && field.isVisible) {
const dateFormat = field.dateDisplayFormat; if (!DateTimeFieldValidator.isValidDateTime(field.value)) {
if (!DateFieldValidator.isValidDate(field.value, dateFormat)) {
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE'; field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE';
isValid = false; isValid = false;
} else { } else {
isValid = this.checkDateTime(field, dateFormat); isValid = this.checkDateTime(field);
} }
} }
return isValid; return isValid;
} }
private checkDateTime(field: FormFieldModel, dateFormat: string): boolean { private checkDateTime(field: FormFieldModel): boolean {
let isValid = true; let isValid = true;
const fieldValueDate = DateFnsUtils.parseDate(field.value, dateFormat); const fieldValueDate = new Date(field.value);
const min = new Date(field.minValue); const min = new Date(field.minValue);
if (DateFnsUtils.isBeforeDate(fieldValueDate, min)) { if (isBefore(fieldValueDate, min)) {
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`; field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`;
field.validationSummary.attributes.set( field.validationSummary.attributes.set(
'minValue', 'minValue',
@ -349,24 +348,22 @@ export class MaxDateTimeFieldValidator implements FormFieldValidator {
validate(field: FormFieldModel): boolean { validate(field: FormFieldModel): boolean {
let isValid = true; let isValid = true;
if (this.isSupported(field) && field.value && field.isVisible) { if (this.isSupported(field) && field.value && field.isVisible) {
const dateFormat = field.dateDisplayFormat; if (!DateTimeFieldValidator.isValidDateTime(field.value)) {
if (!DateFieldValidator.isValidDate(field.value, dateFormat)) {
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE'; field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE';
isValid = false; isValid = false;
} else { } else {
isValid = this.checkDateTime(field, dateFormat); isValid = this.checkDateTime(field);
} }
} }
return isValid; return isValid;
} }
private checkDateTime(field: FormFieldModel, dateFormat: string): boolean { private checkDateTime(field: FormFieldModel): boolean {
let isValid = true; let isValid = true;
const fieldValueDate = DateFnsUtils.parseDate(field.value, dateFormat); const fieldValueDate = new Date(field.value);
const max = new Date(field.maxValue); const max = new Date(field.maxValue);
if (DateFnsUtils.isAfterDate(fieldValueDate, max)) { if (isAfter(fieldValueDate, max)) {
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`; field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`;
field.validationSummary.attributes.set( field.validationSummary.attributes.set(
'maxValue', 'maxValue',

View File

@ -6,29 +6,30 @@
<mat-form-field class="adf-date-time-widget" [class.adf-left-label-input-datepicker]="field.leftLabels" [hideRequiredMarker]="true"> <mat-form-field class="adf-date-time-widget" [class.adf-left-label-input-datepicker]="field.leftLabels" [hideRequiredMarker]="true">
<label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk" *ngIf="isRequired()">*</span></label> <label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input matInput <input matInput
[matDatetimepicker]="datetimePicker"
[id]="field.id" [id]="field.id"
[value]="field.value" [(ngModel)]="value"
[required]="isRequired()" [required]="isRequired()"
[disabled]="field.readOnly" [disabled]="field.readOnly"
(change)="onDateChanged($any($event).srcElement.value)" (change)="onValueChanged($event)"
(dateChange)="onDateChanged($event)"
[placeholder]="field.placeholder" [placeholder]="field.placeholder"
[matTooltip]="field.tooltip" [matTooltip]="field.tooltip"
(blur)="markAsTouched()" (blur)="markAsTouched()"
matTooltipPosition="above" matTooltipPosition="above"
matTooltipShowDelay="1000" matTooltipShowDelay="1000"
(focus)="datetimePicker.open()"> [min]="minDate"
[max]="maxDate">
<mat-datetimepicker-toggle matSuffix [for]="datetimePicker" [disabled]="field.readOnly"></mat-datetimepicker-toggle> <mat-datetimepicker-toggle matSuffix [for]="datetimePicker" [disabled]="field.readOnly"></mat-datetimepicker-toggle>
</mat-form-field> </mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget> <error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget> <error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datetimepicker #datetimePicker type="datetime" [touchUi]="true" [timeInterval]="5" [disabled]="field.readOnly"></mat-datetimepicker> <mat-datetimepicker #datetimePicker
<input type="datetime"
type="hidden" [touchUi]="true"
[matDatetimepicker]="datetimePicker" [openOnFocus]="true"
[value]="field.value | adfMomentDate: field.dateDisplayFormat" [timeInterval]="5"
[min]="minDate" [disabled]="field.readOnly">
[max]="maxDate" </mat-datetimepicker>
[disabled]="field.readOnly"
(dateInput)="onDateChanged($any($event).targetElement.value)">
</div> </div>
</div> </div>

View File

@ -16,7 +16,6 @@
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import moment from 'moment';
import { FormFieldModel } from '../core/form-field.model'; import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model'; import { FormModel } from '../core/form.model';
import { DateTimeWidgetComponent } from './date-time.widget'; import { DateTimeWidgetComponent } from './date-time.widget';
@ -25,12 +24,14 @@ import { TranslateModule } from '@ngx-translate/core';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { FormFieldTypes } from '../core/form-field-types'; import { FormFieldTypes } from '../core/form-field-types';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DateFieldValidator, DateTimeFieldValidator } from '../core';
describe('DateTimeWidgetComponent', () => { describe('DateTimeWidgetComponent', () => {
let widget: DateTimeWidgetComponent; let widget: DateTimeWidgetComponent;
let fixture: ComponentFixture<DateTimeWidgetComponent>; let fixture: ComponentFixture<DateTimeWidgetComponent>;
let element: HTMLElement; let element: HTMLElement;
let form: FormModel;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -44,6 +45,9 @@ describe('DateTimeWidgetComponent', () => {
element = fixture.nativeElement; element = fixture.nativeElement;
widget = fixture.componentInstance; widget = fixture.componentInstance;
form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new DateTimeFieldValidator() ];
}); });
afterEach(() => { afterEach(() => {
@ -51,9 +55,9 @@ describe('DateTimeWidgetComponent', () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
}); });
it('should setup min value for date picker', () => { it('should setup min value for date picker', async () => {
const minValue = '1982-03-13T10:00:000Z'; const minValue = '1982-03-13T10:00:00Z';
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
type: 'datetime', type: 'datetime',
@ -61,13 +65,13 @@ describe('DateTimeWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expected = moment(minValue, 'YYYY-MM-DDTHH:mm:ssZ'); expect(widget.minDate.toISOString()).toBe(`1982-03-13T10:00:00.000Z`);
expect(widget.minDate.isSame(expected)).toBeTruthy();
}); });
it('should date field be present', () => { it('should date field be present', () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
type: 'datetime' type: 'datetime'
@ -78,35 +82,161 @@ describe('DateTimeWidgetComponent', () => {
expect(element.querySelector('#data-time-widget')).not.toBeNull(); expect(element.querySelector('#data-time-widget')).not.toBeNull();
}); });
it('should setup max value for date picker', () => { it('should setup max value for date picker', async () => {
const maxValue = '1982-03-13T10:00:000Z'; const maxValue = '1982-03-13T10:00:00Z';
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(null, {
maxValue maxValue
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expected = moment(maxValue, 'YYYY-MM-DDTHH:mm:ssZ'); expect(widget.maxDate.toISOString()).toBe('1982-03-13T10:00:00.000Z');
expect(widget.maxDate.isSame(expected)).toBeTruthy();
}); });
it('should eval visibility on date changed', () => { it('should eval visibility on date changed', () => {
spyOn(widget, 'onFieldChanged').and.callThrough(); spyOn(widget, 'onFieldChanged').and.callThrough();
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '09-12-9999 10:00 AM', value: '9999-09-12T09:00:00.000Z',
type: 'datetime', type: 'datetime'
readOnly: 'false'
}); });
widget.field = field; widget.field = field;
const mockDate = moment('1982-03-13T10:00:000Z', 'YYYY-MM-DDTHH:mm:ssZ'); widget.onDateChanged({ value: new Date('1982-03-13T10:00:00.000Z') } as any);
widget.onDateChanged(mockDate);
expect(widget.onFieldChanged).toHaveBeenCalledWith(field); expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
}); });
it('should validate the initial datetime value', async () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
});
widget.field = field;
fixture.whenStable();
await fixture.whenStable();
expect(field.isValid).toBeTrue();
});
it('should validate the updated datetime value', async () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
});
widget.field = field;
fixture.whenStable();
await fixture.whenStable();
widget.onDateChanged({ value: new Date('9999-09-12T09:10:00.000Z') } as any);
expect(field.value).toBe('9999-09-12T09:10:00.000Z');
expect(field.isValid).toBeTrue();
});
it('should forwad the incorrect datetime input for further validation', async () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
});
widget.field = field;
fixture.detectChanges();
await fixture.whenStable();
widget.onDateChanged({
value: null,
targetElement: {
value: '123abc'
}
} as any);
fixture.detectChanges();
await fixture.whenStable();
expect(field.value).toBe('123abc');
expect(field.isValid).toBeFalse();
expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A');
});
it('should process direct keyboard input', async () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
});
widget.field = field;
fixture.whenStable();
await fixture.whenStable();
widget.onValueChanged({ target: { value: '9999-09-12T09:10:00.000Z' } } as any);
expect(field.value).toBe('9999-09-12T09:10:00.000Z');
expect(field.isValid).toBeTrue();
});
it('should fail validating incorrect keyboard input', async () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
});
widget.field = field;
fixture.detectChanges();
await fixture.whenStable();
widget.onValueChanged({
target: {
value: '123abc'
}
} as any);
fixture.detectChanges();
await fixture.whenStable();
expect(field.value).toBe('123abc');
expect(field.isValid).toBeFalse();
expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A');
});
it('should allow empty dates when not required', async () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
});
widget.field = field;
fixture.whenStable();
await fixture.whenStable();
widget.onDateChanged({ value: null, targetElement: { value: '' } } as any);
expect(field.value).toBe('');
expect(field.isValid).toBeTrue();
});
describe('when tooltip is set', () => { describe('when tooltip is set', () => {
beforeEach(() => { beforeEach(() => {
@ -166,97 +296,95 @@ describe('DateTimeWidgetComponent', () => {
it('should be able to display label with asterisk', () => { it('should be able to display label with asterisk', () => {
fixture.detectChanges(); fixture.detectChanges();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk'); const asterisk = element.querySelector<HTMLElement>('.adf-asterisk');
expect(asterisk).toBeTruthy(); expect(asterisk).not.toBeNull();
expect(asterisk.textContent).toEqual('*'); expect(asterisk?.textContent).toEqual('*');
}); });
}); });
describe('template check', () => { describe('template check', () => {
it('should show visible date widget', () => { it('should show visible date widget', async () => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '30-11-9999 10:30 AM', value: '9999-11-30T10:30:00.000Z',
type: 'datetime', type: 'datetime'
readOnly: 'false'
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined(); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(element.querySelector('#date-field-id')).not.toBeNull(); expect(dateElement).not.toBeNull();
expect(dateElement?.value).toBe('30-11-9999 10:30 AM');
const dateElement: any = element.querySelector('#date-field-id');
expect(dateElement.value).toBe('30-11-9999 10:30 AM');
}); });
it('should show the correct format type', () => { it('should show the correct format type', async () => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '12-30-9999 10:30 AM', value: '9999-12-30T10:30:00.000Z',
dateDisplayFormat: 'MM-DD-YYYY HH:mm A', dateDisplayFormat: 'MM-DD-YYYY HH:mm A',
type: 'datetime', type: 'datetime'
readOnly: 'false'
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined(); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(element.querySelector('#date-field-id')).not.toBeNull(); expect(dateElement).not.toBeNull();
expect(dateElement?.value).toContain('12-30-9999 10:30 AM');
const dateElement: any = element.querySelector('#date-field-id');
expect(dateElement.value).toContain('12-30-9999 10:30 AM');
}); });
it('should disable date button when is readonly', () => { it('should disable date button when is readonly', () => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '12-30-9999 10:30 AM', value: '9999-12-30T10:30:00.000Z',
dateDisplayFormat: 'MM-DD-YYYY HH:mm A', dateDisplayFormat: 'MM-DD-YYYY HH:mm A',
type: 'datetime', type: 'datetime'
readOnly: 'false'
}); });
fixture.detectChanges(); fixture.detectChanges();
let dateButton = element.querySelector<HTMLButtonElement>('button'); let dateButton = element.querySelector<HTMLButtonElement>('button');
expect(dateButton.disabled).toBeFalsy(); expect(dateButton).not.toBeNull();
expect(dateButton?.disabled).toBeFalsy();
widget.field.readOnly = true; widget.field.readOnly = true;
fixture.detectChanges(); fixture.detectChanges();
dateButton = element.querySelector<HTMLButtonElement>('button'); dateButton = element.querySelector<HTMLButtonElement>('button');
expect(dateButton.disabled).toBeTruthy(); expect(dateButton).not.toBeNull();
expect(dateButton?.disabled).toBeTruthy();
}); });
}); });
it('should display always the json value', () => { it('should display always the json value', async () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'datetime-field-name', name: 'datetime-field-name',
value: '12-30-9999 10:30 AM', value: '9999-12-30T10:30:00.000Z',
type: 'datetime', type: 'datetime',
readOnly: 'false' dateDisplayFormat: 'MM-DD-YYYY HH:mm A'
}); });
field.isVisible = true;
field.dateDisplayFormat = 'MM-DD-YYYY HH:mm A';
widget.field = field; widget.field = field;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined(); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(element.querySelector('#date-field-id')).not.toBeNull(); expect(dateElement).not.toBeNull();
expect(dateElement?.value).toContain('12-30-9999 10:30 AM');
const dateElement: any = element.querySelector('#date-field-id'); widget.field.value = '2020-03-02T00:00:00.000Z';
expect(dateElement.value).toContain('12-30-9999 10:30 AM');
widget.field.value = '03-02-2020 12:00 AM'; fixture.componentInstance.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(dateElement.value).toContain('03-02-2020 12:00 AM'); expect(dateElement?.value).toContain('03-02-2020 00:00 AM');
}); });
describe('when form model has left labels', () => { describe('when form model has left labels', () => {

View File

@ -17,76 +17,88 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { Component, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core'; import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { DatetimeAdapter, MAT_DATETIME_FORMATS } from '@mat-datetimepicker/core'; import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
import { MomentDatetimeAdapter, MAT_MOMENT_DATETIME_FORMATS } from '@mat-datetimepicker/moment';
import moment, { Moment } from 'moment';
import { UserPreferencesService, UserPreferenceValues } from '../../../../common/services/user-preferences.service';
import { MomentDateAdapter } from '../../../../common/utils/moment-date-adapter';
import { MOMENT_DATE_FORMATS } from '../../../../common/utils/moment-date-formats.model';
import { FormService } from '../../../services/form.service'; import { FormService } from '../../../services/form.service';
import { WidgetComponent } from '../widget.component'; import { WidgetComponent } from '../widget.component';
import { Subject } from 'rxjs'; import { ADF_DATE_FORMATS, AdfDateFnsAdapter } from '../../../../common/utils/date-fns-adapter';
import { takeUntil } from 'rxjs/operators'; import { ADF_DATETIME_FORMATS, AdfDateTimeFnsAdapter } from '../../../../common/utils/datetime-fns-adapter';
import { DateFnsUtils } from '../../../../common';
import { isValid } from 'date-fns';
@Component({ @Component({
providers: [ providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter }, { provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }, { provide: MAT_DATETIME_FORMATS, useValue: ADF_DATETIME_FORMATS },
{ provide: DatetimeAdapter, useClass: MomentDatetimeAdapter }, { provide: DateAdapter, useClass: AdfDateFnsAdapter },
{ provide: MAT_DATETIME_FORMATS, useValue: MAT_MOMENT_DATETIME_FORMATS } { provide: DatetimeAdapter, useClass: AdfDateTimeFnsAdapter }
], ],
selector: 'date-time-widget', selector: 'date-time-widget',
templateUrl: './date-time.widget.html', templateUrl: './date-time.widget.html',
styleUrls: ['./date-time.widget.scss'], styleUrls: ['./date-time.widget.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class DateTimeWidgetComponent extends WidgetComponent implements OnInit, OnDestroy { export class DateTimeWidgetComponent extends WidgetComponent implements OnInit {
minDate: Date;
maxDate: Date;
minDate: Moment; @Input()
maxDate: Moment; value: any = null;
private onDestroy$ = new Subject<boolean>();
constructor(public formService: FormService, constructor(public formService: FormService,
private dateAdapter: DateAdapter<Moment>, private dateAdapter: DateAdapter<Date>,
private userPreferencesService: UserPreferencesService) { private dateTimeAdapter: DatetimeAdapter<Date>) {
super(formService); super(formService);
} }
ngOnInit() { ngOnInit() {
this.userPreferencesService if (this.field.dateDisplayFormat) {
.select(UserPreferenceValues.Locale) const dateAdapter = this.dateAdapter as AdfDateFnsAdapter;
.pipe(takeUntil(this.onDestroy$)) dateAdapter.displayFormat = this.field.dateDisplayFormat;
.subscribe(locale => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as MomentDateAdapter; const dateTimeAdapter = this.dateTimeAdapter as AdfDateTimeFnsAdapter;
momentDateAdapter.overrideDisplayFormat = this.field.dateDisplayFormat; dateTimeAdapter.displayFormat = this.field.dateDisplayFormat;
}
if (this.field) { if (this.field) {
if (this.field.minValue) { if (this.field.minValue) {
this.minDate = moment.utc(this.field.minValue, 'YYYY-MM-DDTHH:mm:ssZ'); this.minDate = DateFnsUtils.localToUtc(new Date(this.field.minValue));
} }
if (this.field.maxValue) { if (this.field.maxValue) {
this.maxDate = moment.utc(this.field.maxValue, 'YYYY-MM-DDTHH:mm:ssZ'); this.maxDate = DateFnsUtils.localToUtc(new Date(this.field.maxValue));
}
if (this.field.value) {
this.value = DateFnsUtils.localToUtc(new Date(this.field.value));
} }
} }
} }
ngOnDestroy() { onValueChanged(event: Event) {
this.onDestroy$.next(true); const input = event.target as HTMLInputElement;
this.onDestroy$.complete(); const newValue = this.dateTimeAdapter.parse(input.value, this.field.dateDisplayFormat);
}
onDateChanged(newDateValue) { if (isValid(newValue)) {
const date = moment(newDateValue, this.field.dateDisplayFormat, true); this.field.value = DateFnsUtils.utcToLocal(newValue).toISOString();
if (date.isValid()) {
this.field.value = moment(date).utc().local().format(this.field.dateDisplayFormat);
} else { } else {
this.field.value = newDateValue; this.field.value = input.value;
} }
this.onFieldChanged(this.field);
}
onDateChanged(event: MatDatetimepickerInputEvent<Date>) {
const newValue = event.value;
const input = event.targetElement as HTMLInputElement;
if (newValue && isValid(newValue)) {
this.field.value = DateFnsUtils.utcToLocal(newValue).toISOString();
} else {
this.field.value = input.value;
}
this.onFieldChanged(this.field); this.onFieldChanged(this.field);
} }
} }

View File

@ -23,13 +23,12 @@ import { FormService } from '../../../services/form.service';
import { WidgetComponent } from '../widget.component'; import { WidgetComponent } from '../widget.component';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { MatDatepickerInputEvent } from '@angular/material/datepicker'; import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import { ADF_FORM_DATE_FORMATS } from '../../../date-formats'; import { ADF_DATE_FORMATS, AdfDateFnsAdapter } from '../../../../common/utils/date-fns-adapter';
import { AdfDateFnsAdapter } from '../../../../common/utils/date-fns-adapter';
@Component({ @Component({
selector: 'date-widget', selector: 'date-widget',
providers: [ providers: [
{ provide: MAT_DATE_FORMATS, useValue: ADF_FORM_DATE_FORMATS }, { provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: DateAdapter, useClass: AdfDateFnsAdapter } { provide: DateAdapter, useClass: AdfDateFnsAdapter }
], ],
templateUrl: './date.widget.html', templateUrl: './date.widget.html',

View File

@ -7,29 +7,26 @@
<mat-form-field class="adf-date-widget" [class.adf-left-label-input-datepicker]="field.leftLabels" [hideRequiredMarker]="true"> <mat-form-field class="adf-date-widget" [class.adf-left-label-input-datepicker]="field.leftLabels" [hideRequiredMarker]="true">
<label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk" <label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label> *ngIf="isRequired()">*</span></label>
<input matInput <input matInput [matDatepicker]="datePicker"
[id]="field.id" [id]="field.id"
[value]="field.value" [(ngModel)]="value"
[required]="isRequired()" [required]="field.required"
[disabled]="field.readOnly"
(change)="onDateChanged($any($event).srcElement.value)"
[placeholder]="field.placeholder" [placeholder]="field.placeholder"
[matTooltip]="field.tooltip"
(blur)="markAsTouched()"
matTooltipPosition="above"
matTooltipShowDelay="1000">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly" ></mat-datepicker-toggle>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datepicker #datePicker [touchUi]="true" [startAt]="field.value | adfMomentDate: field.dateDisplayFormat" [disabled]="field.readOnly"></mat-datepicker>
<input
type="hidden"
[matDatepicker]="datePicker"
[value]="field.value | adfMomentDate: field.dateDisplayFormat"
[min]="minDate" [min]="minDate"
[max]="maxDate" [max]="maxDate"
[disabled]="field.readOnly" [disabled]="field.readOnly"
(dateInput)="onDateChanged($any($event).targetElement.value)"> [matTooltip]="field.tooltip"
matTooltipPosition="above"
matTooltipShowDelay="1000"
(dateChange)="onDateChanged($event)"
(blur)="markAsTouched()">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly"></mat-datepicker-toggle>
<mat-datepicker #datePicker
[startAt]="startAt"
[disabled]="field.readOnly">
</mat-datepicker>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div> </div>
</div> </div>

View File

@ -17,18 +17,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DateCloudWidgetComponent } from './date-cloud.widget'; import { DateCloudWidgetComponent } from './date-cloud.widget';
import { FormFieldModel, FormModel, FormFieldTypes } from '@alfresco/adf-core'; import { FormFieldModel, FormModel, FormFieldTypes, DateFieldValidator, MinDateFieldValidator, MaxDateFieldValidator } from '@alfresco/adf-core';
import moment from 'moment';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module'; import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { DATE_FORMAT_CLOUD } from '../../../../models/date-format-cloud.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DateAdapter } from '@angular/material/core';
import { isEqual, subDays, addDays } from 'date-fns';
describe('DateWidgetComponent', () => { describe('DateWidgetComponent', () => {
let widget: DateCloudWidgetComponent; let widget: DateCloudWidgetComponent;
let fixture: ComponentFixture<DateCloudWidgetComponent>; let fixture: ComponentFixture<DateCloudWidgetComponent>;
let element: HTMLElement; let element: HTMLElement;
let adapter: DateAdapter<Date>;
let form: FormModel;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -37,7 +39,13 @@ describe('DateWidgetComponent', () => {
ProcessServiceCloudTestingModule ProcessServiceCloudTestingModule
] ]
}); });
form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new MinDateFieldValidator(), new MaxDateFieldValidator()];
fixture = TestBed.createComponent(DateCloudWidgetComponent); fixture = TestBed.createComponent(DateCloudWidgetComponent);
adapter = fixture.debugElement.injector.get(DateAdapter);
widget = fixture.componentInstance; widget = fixture.componentInstance;
element = fixture.nativeElement; element = fixture.nativeElement;
}); });
@ -52,13 +60,14 @@ describe('DateWidgetComponent', () => {
widget.ngOnInit(); widget.ngOnInit();
const expected = moment(minValue, DATE_FORMAT_CLOUD); const expected = adapter.parse(minValue, widget.DATE_FORMAT);
expect(widget.minDate.isSame(expected)).toBeTruthy(); expect(isEqual(widget.minDate, expected)).toBeTrue();
}); });
it('should date field be present', () => { it('should date field be present', () => {
const minValue = '1982-03-13'; const minValue = '1982-03-13';
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
minValue minValue
}); });
@ -70,29 +79,29 @@ describe('DateWidgetComponent', () => {
it('should setup max value for date picker', () => { it('should setup max value for date picker', () => {
const maxValue = '1982-03-13'; const maxValue = '1982-03-13';
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
maxValue maxValue
}); });
widget.ngOnInit(); widget.ngOnInit();
const expected = moment(maxValue, DATE_FORMAT_CLOUD); const expected = adapter.parse(maxValue, widget.DATE_FORMAT);
expect(widget.maxDate.isSame(expected)).toBeTruthy(); expect(isEqual(widget.maxDate, expected)).toBeTrue();
}); });
it('should eval visibility on date changed', () => { it('should eval visibility on date changed', () => {
spyOn(widget, 'onFieldChanged').and.callThrough(); spyOn(widget, 'onFieldChanged').and.callThrough();
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-9-9', value: '9999-9-9',
type: 'date',
readOnly: 'false' readOnly: 'false'
}); });
widget.field = field; widget.field = field;
const todayDate = moment().format(DATE_FORMAT_CLOUD); widget.onDateChanged({ value: adapter.today() } as any);
widget.onDateChanged({ value: todayDate });
expect(widget.onFieldChanged).toHaveBeenCalledWith(field); expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
}); });
@ -105,48 +114,46 @@ describe('DateWidgetComponent', () => {
}); });
it('should show visible date widget', async () => { it('should show visible date widget', async () => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
// always stored as dd-MM-yyyy
value: '9999-9-9', value: '9999-9-9',
type: 'date', type: FormFieldTypes.DATE
readOnly: 'false'
}); });
widget.field.isVisible = true;
widget.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement.value).toContain('9-9-9999'); expect(dateElement).not.toBeNull();
expect(dateElement?.value).toContain('9-9-9999');
}); });
it('should show the correct format type', async () => { it('should show the correct format type', async () => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-30-12', // always stored as dd-MM-yyyy
type: 'date', value: '30-12-9999',
readOnly: 'false' type: FormFieldTypes.DATE,
dateDisplayFormat: 'YYYY-DD-MM'
}); });
widget.field.isVisible = true;
widget.field.dateDisplayFormat = 'YYYY-DD-MM';
widget.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined();
expect(element.querySelector('#date-field-id')).not.toBeNull();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement.value).toContain('9999-30-12'); expect(dateElement.value).toContain('9999-30-12');
}); });
it('should disable date button when is readonly', async () => { it('should disable date button when is readonly', async () => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-9-9', value: '9999-9-9',
type: 'date', type: FormFieldTypes.DATE,
readOnly: 'false' readOnly: 'false'
}); });
widget.field.isVisible = true; widget.field.isVisible = true;
@ -170,7 +177,7 @@ describe('DateWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: 'aa', value: 'aa',
type: 'date', type: FormFieldTypes.DATE,
readOnly: 'false' readOnly: 'false'
}); });
widget.field.isVisible = true; widget.field.isVisible = true;
@ -184,31 +191,31 @@ describe('DateWidgetComponent', () => {
}); });
it('should display always the json value', async () => { it('should display always the json value', async () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '12-30-9999', // always stored as dd-MM-yyyy
type: 'date', value: '30-12-9999',
readOnly: 'false' type: FormFieldTypes.DATE,
readOnly: 'false',
dateDisplayFormat: 'MM-DD-YYYY'
}); });
field.isVisible = true;
field.dateDisplayFormat = 'MM-DD-YYYY';
widget.field = field; widget.field = field;
widget.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(element.querySelector('#date-field-id')).toBeDefined(); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(element.querySelector('#date-field-id')).not.toBeNull(); expect(dateElement).toBeDefined();
const dateElement: any = element.querySelector('#date-field-id');
expect(dateElement.value).toContain('12-30-9999'); expect(dateElement.value).toContain('12-30-9999');
widget.field.value = '03-02-2020'; widget.field.value = '03-02-2020';
fixture.componentInstance.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
expect(dateElement.value).toContain('03-02-2020'); expect(dateElement.value).toContain('02-03-2020');
}); });
describe('when form model has left labels', () => { describe('when form model has left labels', () => {
@ -285,7 +292,8 @@ describe('DateWidgetComponent', () => {
describe('Set dynamic dates', () => { describe('Set dynamic dates', () => {
it('should min date equal to the today date minus minimum date range value', async () => { it('should min date equal to the today date minus minimum date range value', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
minDateRangeValue: 4 minDateRangeValue: 4
}); });
@ -293,13 +301,13 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
const todayDate = moment().format(DATE_FORMAT_CLOUD); const expected = subDays(adapter.today(), widget.field.minDateRangeValue);
const expected = moment(todayDate).subtract(widget.field.minDateRangeValue, 'days'); expect(widget.minDate.toDateString()).toBe(expected.toDateString());
expect(widget.minDate).toEqual(expected);
}); });
it('should min date and max date be undefined if dynamic min and max date are not set', async () => { it('should min date and max date be undefined if dynamic min and max date are not set', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true dynamicDateRangeSelection: true
}); });
@ -311,7 +319,8 @@ describe('DateWidgetComponent', () => {
}); });
it('should max date be undefined if only minimum date range value is set', async () => { it('should max date be undefined if only minimum date range value is set', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
minDateRangeValue: 4 minDateRangeValue: 4
}); });
@ -323,7 +332,8 @@ describe('DateWidgetComponent', () => {
}); });
it('should min date be undefined if only maximum date range value is set', async () => { it('should min date be undefined if only maximum date range value is set', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: 4 maxDateRangeValue: 4
}); });
@ -335,7 +345,8 @@ describe('DateWidgetComponent', () => {
}); });
it('should max date equal to the today date plus maximum date range value', async () => { it('should max date equal to the today date plus maximum date range value', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: 5 maxDateRangeValue: 5
}); });
@ -343,13 +354,13 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
const todayDate = moment().format(DATE_FORMAT_CLOUD); const expected = addDays(adapter.today(), widget.field.maxDateRangeValue);
const expected = moment(todayDate).add(widget.field.maxDateRangeValue, 'days'); expect(widget.maxDate.toDateString()).toBe(expected.toDateString());
expect(widget.maxDate).toEqual(expected);
}); });
it('should maxDate and minDate be undefined if minDateRangeValue and maxDateRangeValue are null', async () => { it('should maxDate and minDate be undefined if minDateRangeValue and maxDateRangeValue are null', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: null, maxDateRangeValue: null,
minDateRangeValue: null minDateRangeValue: null
@ -363,7 +374,8 @@ describe('DateWidgetComponent', () => {
}); });
it('should minDate be undefined if minDateRangeValue is null and maxDateRangeValue is greater than 0', async () => { it('should minDate be undefined if minDateRangeValue is null and maxDateRangeValue is greater than 0', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: 15, maxDateRangeValue: 15,
minDateRangeValue: null minDateRangeValue: null
@ -377,7 +389,8 @@ describe('DateWidgetComponent', () => {
}); });
it('should maxDate be undefined if maxDateRangeValue is null and minDateRangeValue is greater than 0', async () => { it('should maxDate be undefined if maxDateRangeValue is null and minDateRangeValue is greater than 0', async () => {
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: null, maxDateRangeValue: null,
minDateRangeValue: 10 minDateRangeValue: 10
@ -392,8 +405,10 @@ describe('DateWidgetComponent', () => {
describe('check date validation by dynamic date ranges', () => { describe('check date validation by dynamic date ranges', () => {
it('should minValue be equal to today date minus minDateRangeValue', async () => { it('should minValue be equal to today date minus minDateRangeValue', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22'); spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: null, maxDateRangeValue: null,
minDateRangeValue: 1, minDateRangeValue: 1,
@ -404,16 +419,16 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
const expectedMinValueString = '2022-07-21'; expect(widget.field.minValue).toEqual('21-07-2022');
expect(widget.field.minValue).toEqual(expectedMinValueString);
expect(widget.maxDate).toBeUndefined(); expect(widget.maxDate).toBeUndefined();
expect(widget.field.maxValue).toBeNull(); expect(widget.field.maxValue).toBeNull();
}); });
it('should maxValue be equal to today date plus maxDateRangeValue', async () => { it('should maxValue be equal to today date plus maxDateRangeValue', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22'); spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: 8, maxDateRangeValue: 8,
minDateRangeValue: null, minDateRangeValue: null,
@ -424,16 +439,16 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
const expectedMaxValueString = '2022-07-30'; expect(widget.field.maxValue).toEqual('30-07-2022');
expect(widget.field.maxValue).toEqual(expectedMaxValueString);
expect(widget.minDate).toBeUndefined(); expect(widget.minDate).toBeUndefined();
expect(widget.field.minValue).toBeNull(); expect(widget.field.minValue).toBeNull();
}); });
it('should maxValue and minValue be null if maxDateRangeValue and minDateRangeValue are null', async () => { it('should maxValue and minValue be null if maxDateRangeValue and minDateRangeValue are null', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22'); spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: null, maxDateRangeValue: null,
minDateRangeValue: null, minDateRangeValue: null,
@ -451,8 +466,10 @@ describe('DateWidgetComponent', () => {
}); });
it('should maxValue and minValue not be null if maxDateRangeVale and minDateRangeValue are not null', async () => { it('should maxValue and minValue not be null if maxDateRangeVale and minDateRangeValue are not null', async () => {
spyOn(widget, 'getTodaysFormattedDate').and.returnValue('2022-07-22'); spyOn(adapter, 'today').and.returnValue(new Date('2022-07-22'));
widget.field = new FormFieldModel(null, {
widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
maxDateRangeValue: 8, maxDateRangeValue: 8,
minDateRangeValue: 10, minDateRangeValue: 10,
@ -463,11 +480,8 @@ describe('DateWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
const expectedMaxValueString = '2022-07-30'; expect(widget.field.minValue).toEqual('12-07-2022');
const expectedMinValueString = '2022-07-12'; expect(widget.field.maxValue).toEqual('30-07-2022');
expect(widget.field.maxValue).toEqual(expectedMaxValueString);
expect(widget.field.minValue).toEqual(expectedMinValueString);
}); });
}); });
}); });

View File

@ -17,22 +17,20 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { Component, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core'; import { Component, OnInit, ViewEncapsulation, OnDestroy, Input } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import moment, { Moment } from 'moment';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { WidgetComponent, FormService, AdfDateFnsAdapter, DateFnsUtils } from '@alfresco/adf-core';
import { import { MatDatepickerInputEvent } from '@angular/material/datepicker';
MOMENT_DATE_FORMATS, MomentDateAdapter, WidgetComponent, import { CLOUD_FORM_DATE_FORMATS } from '../../../date-formats';
UserPreferencesService, UserPreferenceValues, FormService import { addDays, subDays } from 'date-fns';
} from '@alfresco/adf-core';
import { DATE_FORMAT_CLOUD } from '../../../../models/date-format-cloud.model';
@Component({ @Component({
selector: 'date-widget', selector: 'date-widget',
providers: [ providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter }, { provide: MAT_DATE_FORMATS, useValue: CLOUD_FORM_DATE_FORMATS },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }], { provide: DateAdapter, useClass: AdfDateFnsAdapter }
],
templateUrl: './date-cloud.widget.html', templateUrl: './date-cloud.widget.html',
styleUrls: ['./date-cloud.widget.scss'], styleUrls: ['./date-cloud.widget.scss'],
host: { host: {
@ -50,52 +48,58 @@ import { DATE_FORMAT_CLOUD } from '../../../../models/date-format-cloud.model';
}) })
export class DateCloudWidgetComponent extends WidgetComponent implements OnInit, OnDestroy { export class DateCloudWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
typeId = 'DateCloudWidgetComponent'; typeId = 'DateCloudWidgetComponent';
readonly DATE_FORMAT = 'dd-MM-yyyy';
minDate: Moment; minDate: Date;
maxDate: Moment; maxDate: Date;
startAt: Date;
/**
* Current date value.
* The value is always stored in the format `dd-MM-yyyy`,
* but displayed in the UI component using `dateDisplayFormat`
*/
@Input()
value: any = null;
private onDestroy$ = new Subject<boolean>(); private onDestroy$ = new Subject<boolean>();
constructor(public formService: FormService, constructor(public formService: FormService,
private dateAdapter: DateAdapter<Moment>, private dateAdapter: DateAdapter<Date>) {
private userPreferencesService: UserPreferencesService) {
super(formService); super(formService);
} }
ngOnInit() { ngOnInit() {
this.userPreferencesService if (this.field.dateDisplayFormat) {
.select(UserPreferenceValues.Locale) const adapter = this.dateAdapter as AdfDateFnsAdapter;
.pipe(takeUntil(this.onDestroy$)) adapter.displayFormat = this.field.dateDisplayFormat;
.subscribe(locale => this.dateAdapter.setLocale(locale)); }
const momentDateAdapter = this.dateAdapter as MomentDateAdapter;
momentDateAdapter.overrideDisplayFormat = this.field.dateDisplayFormat;
if (this.field) { if (this.field) {
if (this.field.dynamicDateRangeSelection) { if (this.field.dynamicDateRangeSelection) {
const today = this.getTodaysFormattedDate();
if (Number.isInteger(this.field.minDateRangeValue)) { if (Number.isInteger(this.field.minDateRangeValue)) {
this.minDate = moment(today).subtract(this.field.minDateRangeValue, 'days'); this.minDate = subDays(this.dateAdapter.today(), this.field.minDateRangeValue);
this.field.minValue = this.minDate.format(DATE_FORMAT_CLOUD); this.field.minValue = DateFnsUtils.formatDate(this.minDate, this.DATE_FORMAT);
} }
if (Number.isInteger(this.field.maxDateRangeValue)) { if (Number.isInteger(this.field.maxDateRangeValue)) {
this.maxDate = moment(today).add(this.field.maxDateRangeValue, 'days'); this.maxDate = addDays(this.dateAdapter.today(), this.field.maxDateRangeValue);
this.field.maxValue = this.maxDate.format(DATE_FORMAT_CLOUD); this.field.maxValue = DateFnsUtils.formatDate(this.maxDate, this.DATE_FORMAT);
} }
} else { } else {
if (this.field.minValue) { if (this.field.minValue) {
this.minDate = moment(this.field.minValue, DATE_FORMAT_CLOUD); this.minDate = this.dateAdapter.parse(this.field.minValue, this.DATE_FORMAT);
} }
if (this.field.maxValue) { if (this.field.maxValue) {
this.maxDate = moment(this.field.maxValue, DATE_FORMAT_CLOUD); this.maxDate = this.dateAdapter.parse(this.field.maxValue, this.DATE_FORMAT);
}
}
} }
} }
getTodaysFormattedDate() { if (this.field.value) {
return moment().format(DATE_FORMAT_CLOUD); this.startAt = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
this.value = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
}
}
} }
ngOnDestroy() { ngOnDestroy() {
@ -103,13 +107,16 @@ export class DateCloudWidgetComponent extends WidgetComponent implements OnInit,
this.onDestroy$.complete(); this.onDestroy$.complete();
} }
onDateChanged(newDateValue) { onDateChanged(event: MatDatepickerInputEvent<Date>) {
const date = moment(newDateValue, this.field.dateDisplayFormat, true); const value = event.value;
if (date.isValid()) { const input = event.targetElement as HTMLInputElement;
this.field.value = date.format(this.field.dateDisplayFormat);
if (value) {
this.field.value = this.dateAdapter.format(value, this.DATE_FORMAT);
} else { } else {
this.field.value = newDateValue; this.field.value = input.value;
} }
this.onFieldChanged(this.field); this.onFieldChanged(this.field);
} }
} }

View File

@ -17,12 +17,19 @@
import { MatDateFormats } from '@angular/material/core'; import { MatDateFormats } from '@angular/material/core';
export const ADF_FORM_DATE_FORMATS: MatDateFormats = { /**
* Provides date/time display formatting for the cloud components.
*
* Notes for developers: display formats are different from the storage formats.
* Components have a fixed format for saving dates and datetime values,
* while dynamic format for UI display.
*/
export const CLOUD_FORM_DATE_FORMATS: MatDateFormats = {
parse: { parse: {
dateInput: 'dd-MM-yyyy' dateInput: 'yyyy-MM-dd'
}, },
display: { display: {
dateInput: 'dd-MM-yyyy', dateInput: 'yyyy-MM-dd',
monthLabel: 'LLL', monthLabel: 'LLL',
monthYearLabel: 'LLL uuuu', monthYearLabel: 'LLL uuuu',
dateA11yLabel: 'PP', dateA11yLabel: 'PP',

View File

@ -40,3 +40,4 @@ export * from './services/content-cloud-node-selector.service';
export * from './services/process-cloud-content.service'; export * from './services/process-cloud-content.service';
export * from './form-cloud.module'; export * from './form-cloud.module';
export * from './date-formats';

View File

@ -5,11 +5,11 @@
id="dateInput" id="dateInput"
type="text" type="text"
[matDatepicker]="datePicker" [matDatepicker]="datePicker"
[value]="value" [(ngModel)]="value"
[id]="column.id" [id]="column.id"
[required]="column.required" [required]="column.required"
[disabled]="!column.editable" [disabled]="!column.editable"
(focusout)="onDateChanged($any($event).srcElement)" (focusout)="onDateChanged($any($event).target.value)"
(dateChange)="onDateChanged($event)"> (dateChange)="onDateChanged($event)">
<mat-datepicker-toggle *ngIf="column.editable" matSuffix [for]="datePicker" class="adf-date-editor-button" ></mat-datepicker-toggle> <mat-datepicker-toggle *ngIf="column.editable" matSuffix [for]="datePicker" class="adf-date-editor-button" ></mat-datepicker-toggle>
</mat-form-field> </mat-form-field>

View File

@ -22,7 +22,6 @@ import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model'; import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { DateEditorComponent } from './date.editor'; import { DateEditorComponent } from './date.editor';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
describe('DateEditorComponent', () => { describe('DateEditorComponent', () => {
@ -55,7 +54,7 @@ describe('DateEditorComponent', () => {
describe('using Date Piker', () => { describe('using Date Piker', () => {
it('should update row value on change', () => { it('should update row value on change', () => {
const input = {value: '14-03-2016'} as MatDatepickerInputEvent<any>; const input = {value: '2016-03-14'} as any;
component.ngOnInit(); component.ngOnInit();
component.onDateChanged(input); component.onDateChanged(input);
@ -66,7 +65,7 @@ describe('DateEditorComponent', () => {
it('should flush value on user input', () => { it('should flush value on user input', () => {
spyOn(table, 'flushValue').and.callThrough(); spyOn(table, 'flushValue').and.callThrough();
const input = {value: '14-03-2016'} as MatDatepickerInputEvent<any>; const input = { value: '2016-03-14' } as any;
component.ngOnInit(); component.ngOnInit();
component.onDateChanged(input); component.onDateChanged(input);

View File

@ -15,32 +15,29 @@
* limitations under the License. * limitations under the License.
*/ */
/* eslint-disable @angular-eslint/component-selector */ import { ADF_DATE_FORMATS, AdfDateFnsAdapter, DateFnsUtils } from '@alfresco/adf-core';
import { Component, Input, OnInit } from '@angular/core';
import { UserPreferencesService, UserPreferenceValues, MomentDateAdapter, MOMENT_DATE_FORMATS } from '@alfresco/adf-core';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerInputEvent } from '@angular/material/datepicker'; import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import moment, { Moment } from 'moment';
import { DynamicTableColumn } from '../models/dynamic-table-column.model'; import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model'; import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model'; import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { Subject } from 'rxjs'; import { isValid } from 'date-fns';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'adf-date-editor', selector: 'adf-date-editor',
templateUrl: './date.editor.html', templateUrl: './date.editor.html',
providers: [ providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter }, { provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS } { provide: DateAdapter, useClass: AdfDateFnsAdapter }
], ],
styleUrls: ['./date.editor.scss'] styleUrls: ['./date.editor.scss']
}) })
export class DateEditorComponent implements OnInit, OnDestroy { export class DateEditorComponent implements OnInit {
DATE_FORMAT: string = 'DD-MM-YYYY'; DATE_FORMAT: string = 'DD-MM-YYYY';
value: any; @Input()
value: Date;
@Input() @Input()
table: DynamicTableModel; table: DynamicTableModel;
@ -51,43 +48,32 @@ export class DateEditorComponent implements OnInit, OnDestroy {
@Input() @Input()
column: DynamicTableColumn; column: DynamicTableColumn;
minDate: Moment; minDate: Date;
maxDate: Moment; maxDate: Date;
private onDestroy$ = new Subject<boolean>(); constructor(private dateAdapter: DateAdapter<Date>) {}
constructor(private dateAdapter: DateAdapter<Moment>, private userPreferencesService: UserPreferencesService) {}
ngOnInit() { ngOnInit() {
this.userPreferencesService const momentDateAdapter = this.dateAdapter as AdfDateFnsAdapter;
.select(UserPreferenceValues.Locale) momentDateAdapter.displayFormat = this.DATE_FORMAT;
.pipe(takeUntil(this.onDestroy$))
.subscribe((locale) => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as MomentDateAdapter; this.value = this.table.getCellValue(this.row, this.column) as Date;
momentDateAdapter.overrideDisplayFormat = this.DATE_FORMAT;
this.value = moment(this.table.getCellValue(this.row, this.column), this.DATE_FORMAT);
} }
ngOnDestroy() { onDateChanged(newDateValue: MatDatepickerInputEvent<Date> | string) {
this.onDestroy$.next(true); if (typeof newDateValue === 'string') {
this.onDestroy$.complete(); const newValue = DateFnsUtils.parseDate(newDateValue, this.DATE_FORMAT);
}
onDateChanged(newDateValue: MatDatepickerInputEvent<any> | HTMLInputElement) { if (isValid(newValue)) {
if (newDateValue?.value) { this.row.value[this.column.id] = `${DateFnsUtils.formatDate(newValue, 'yyyy-MM-dd')}T00:00:00.000Z`;
/* validates the user inputs */ this.table.flushValue();
const momentDate = moment(newDateValue.value, this.DATE_FORMAT, true); } else {
this.row.value[this.column.id] = newDateValue;
if (!momentDate.isValid()) { }
this.row.value[this.column.id] = newDateValue.value; } else if (newDateValue?.value) {
} else { this.row.value[this.column.id] = `${DateFnsUtils.formatDate(newDateValue?.value, 'yyyy-MM-dd')}T00:00:00.000Z`;
this.row.value[this.column.id] = `${momentDate.format('YYYY-MM-DD')}T00:00:00.000Z`;
this.table.flushValue(); this.table.flushValue();
}
} else { } else {
/* removes the date */
this.row.value[this.column.id] = ''; this.row.value[this.column.id] = '';
} }
} }

View File

@ -7,7 +7,7 @@
[id]="column.id" [id]="column.id"
[required]="column.required" [required]="column.required"
[disabled]="!column.editable" [disabled]="!column.editable"
(focusout)="onDateChanged($any($event).srcElement.value)" (focusout)="onDateChanged($any($event).target.value)"
(dateChange)="onDateChanged($event)"> (dateChange)="onDateChanged($event)">
<mat-datetimepicker-toggle <mat-datetimepicker-toggle
matSuffix matSuffix

View File

@ -16,7 +16,6 @@
*/ */
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import moment from 'moment';
import { FormFieldModel, FormModel, CoreTestingModule } from '@alfresco/adf-core'; import { FormFieldModel, FormModel, CoreTestingModule } from '@alfresco/adf-core';
import { DynamicTableColumn } from '../models/dynamic-table-column.model'; import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model'; import { DynamicTableRow } from '../models/dynamic-table-row.model';
@ -52,26 +51,27 @@ describe('DateTimeEditorComponent', () => {
component.column = column; component.column = column;
}); });
it('should update fow value on change', () => { it('should update row value on change', () => {
component.ngOnInit(); component.ngOnInit();
const newDate = moment('22-6-2018 04:20 AM', 'D-M-YYYY hh:mm A'); const newDate = new Date('2018-6-22 04:20 AM');
component.onDateChanged(newDate); component.onDateChanged({ value: newDate } as any);
expect(moment(row.value[column.id]).isSame(newDate)).toBeTruthy();
expect(row.value[column.id]).toBe('22/06/2018 04:20');
}); });
it('should update row value upon user input', () => { it('should update row value upon user input', () => {
const input = '22-6-2018 04:20 AM'; const input = '22/6/2018 04:20';
component.ngOnInit(); component.ngOnInit();
component.onDateChanged(input); component.onDateChanged(input);
const actual = row.value[column.id]; const actual = row.value[column.id];
expect(actual).toBe('22-6-2018 04:20 AM'); expect(actual).toBe('2018-06-22T04:20:00.000Z');
}); });
it('should flush value on user input', () => { it('should flush value on user input', () => {
spyOn(table, 'flushValue').and.callThrough(); spyOn(table, 'flushValue').and.callThrough();
const input = '22-6-2018 04:20 AM'; const input = '22/6/2018 04:20';
component.ngOnInit(); component.ngOnInit();
component.onDateChanged(input); component.onDateChanged(input);

View File

@ -15,35 +15,31 @@
* limitations under the License. * limitations under the License.
*/ */
/* eslint-disable @angular-eslint/component-selector */ import { ADF_DATETIME_FORMATS, ADF_DATE_FORMATS, AdfDateFnsAdapter, AdfDateTimeFnsAdapter, /*MOMENT_DATE_FORMATS, MomentDateAdapter*/
DateFnsUtils} from '@alfresco/adf-core';
import { MOMENT_DATE_FORMATS, MomentDateAdapter, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core'; import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import moment, { Moment } from 'moment';
import { DynamicTableColumn } from '../models/dynamic-table-column.model'; import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model'; import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model'; import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { DatetimeAdapter, MAT_DATETIME_FORMATS } from '@mat-datetimepicker/core'; import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
import { MomentDatetimeAdapter, MAT_MOMENT_DATETIME_FORMATS } from '@mat-datetimepicker/moment';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'adf-datetime-editor', selector: 'adf-datetime-editor',
templateUrl: './datetime.editor.html', templateUrl: './datetime.editor.html',
providers: [ providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter }, { provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }, { provide: MAT_DATETIME_FORMATS, useValue: ADF_DATETIME_FORMATS },
{ provide: DatetimeAdapter, useClass: MomentDatetimeAdapter }, { provide: DateAdapter, useClass: AdfDateFnsAdapter },
{ provide: MAT_DATETIME_FORMATS, useValue: MAT_MOMENT_DATETIME_FORMATS } { provide: DatetimeAdapter, useClass: AdfDateTimeFnsAdapter }
], ],
styleUrls: ['./datetime.editor.scss'] styleUrls: ['./datetime.editor.scss']
}) })
export class DateTimeEditorComponent implements OnInit, OnDestroy { export class DateTimeEditorComponent implements OnInit {
DATE_TIME_FORMAT: string = 'DD/MM/YYYY HH:mm'; DATE_TIME_FORMAT: string = 'DD/MM/YYYY HH:mm';
value: any; @Input()
value: Date;
@Input() @Input()
table: DynamicTableModel; table: DynamicTableModel;
@ -54,40 +50,28 @@ export class DateTimeEditorComponent implements OnInit, OnDestroy {
@Input() @Input()
column: DynamicTableColumn; column: DynamicTableColumn;
minDate: Moment; minDate: Date;
maxDate: Moment; maxDate: Date;
private onDestroy$ = new Subject<boolean>(); constructor(private dateAdapter: DateAdapter<Date>) {}
constructor(private dateAdapter: DateAdapter<Moment>, private userPreferencesService: UserPreferencesService) {}
ngOnInit() { ngOnInit() {
this.userPreferencesService const momentDateAdapter = this.dateAdapter as AdfDateFnsAdapter;
.select(UserPreferenceValues.Locale) momentDateAdapter.displayFormat = this.DATE_TIME_FORMAT;
.pipe(takeUntil(this.onDestroy$))
.subscribe((locale) => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as MomentDateAdapter; this.value = this.table.getCellValue(this.row, this.column) as Date;
momentDateAdapter.overrideDisplayFormat = this.DATE_TIME_FORMAT;
this.value = moment(this.table.getCellValue(this.row, this.column), this.DATE_TIME_FORMAT);
} }
ngOnDestroy() { onDateChanged(newDateValue: MatDatetimepickerInputEvent<Date> | string) {
this.onDestroy$.next(true); if (typeof newDateValue === 'string') {
this.onDestroy$.complete(); const newValue = DateFnsUtils.parseDate(newDateValue, this.DATE_TIME_FORMAT);
}
onDateChanged(newDateValue) {
if (newDateValue?.value) {
const newValue = moment(newDateValue.value, this.DATE_TIME_FORMAT);
this.row.value[this.column.id] = newDateValue.value.format(this.DATE_TIME_FORMAT);
this.value = newValue; this.value = newValue;
this.row.value[this.column.id] = newValue.toISOString();
this.table.flushValue(); this.table.flushValue();
} else if (newDateValue) { } else if (newDateValue.value) {
const newValue = moment(newDateValue, this.DATE_TIME_FORMAT); const newValue = DateFnsUtils.formatDate(newDateValue.value, this.DATE_TIME_FORMAT);
this.value = newValue; this.row.value[this.column.id] = newValue;
this.row.value[this.column.id] = newDateValue; this.value = newDateValue.value;
this.table.flushValue(); this.table.flushValue();
} else { } else {
this.row.value[this.column.id] = ''; this.row.value[this.column.id] = '';

View File

@ -0,0 +1,94 @@
/*!
* @license
* Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DateCellValidator } from './date-cell-validator-model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
describe('DateCellValidator', () => {
let validator: DateCellValidator;
beforeEach(() => {
validator = new DateCellValidator();
});
it('should require column to validate', () => {
expect(validator.isSupported(null)).toBeFalse();
});
it('should support only editable columns', () => {
const readonly = { editable: false, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
expect(validator.isSupported(readonly)).toBeFalse();
const editable = { editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
expect(validator.isSupported(editable)).toBeTrue();
});
it('should support only date column type', () => {
const date = { editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
expect(validator.isSupported(date)).toBeTrue();
const unsupported = { editable: true, type: 'unknown' } as DynamicTableColumn;
expect(validator.isSupported(unsupported)).toBeFalse();
});
it('should skip validating unsupported columns', () => {
const column = { editable: true, type: 'unknown' } as DynamicTableColumn;
const row = {} as DynamicTableRow;
expect(validator.validate(row, column)).toBeTrue();
});
it('should reject when required column has no value', () => {
const column = { id: 'col1', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: null } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeFalse();
});
it('should approve when optional column has no value', () => {
const column = { id: 'col1', required: false, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: null } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeTrue();
});
it('should approve the valid datetime value', () => {
const column = { id: 'col1', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: '2023-10-12T10:59:24.773Z' } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeTrue();
});
it('should reject invalid datetime value', () => {
const column = { id: 'col1', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: '!2023-10-12T10:59:24.773Z' } } as DynamicTableRow;
expect(validator.validate(row, column)).toBeFalse();
});
it('should update validation summary of rejection', () => {
const column = { id: 'col1', name: 'created_on', required: true, editable: true, type: DateCellValidator.DATE_TYPE } as DynamicTableColumn;
const row = { value: { col1: '!2023-10-12T10:59:24.773Z' } } as DynamicTableRow;
const summary = new DynamicRowValidationSummary();
expect(validator.validate(row, column, summary)).toBeFalse();
expect(summary.isValid).toBeFalse();
expect(summary.message).toBe(`Invalid 'created_on' format.`);
});
});

View File

@ -17,34 +17,40 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import moment from 'moment';
import { CellValidator } from './cell-validator.model'; import { CellValidator } from './cell-validator.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model'; import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model'; import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model'; import { DynamicTableRow } from './dynamic-table-row.model';
import { isValid } from 'date-fns';
export class DateCellValidator implements CellValidator { export class DateCellValidator implements CellValidator {
private supportedTypes: string[] = ['Date']; static DATE_TYPE = 'Date';
private supportedTypes: string[] = [DateCellValidator.DATE_TYPE];
isSupported(column: DynamicTableColumn): boolean { isSupported(column: DynamicTableColumn): boolean {
return column?.editable && this.supportedTypes.indexOf(column.type) > -1; return !!(column?.editable && this.supportedTypes.indexOf(column?.type) > -1);
} }
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean { validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) { if (this.isSupported(column)) {
const value = row.value[column.id]; const value = row?.value[column.id];
if (!value && !column.required) { if (value) {
const dateValue = new Date(value);
if (isValid(dateValue)) {
return true; return true;
} }
const dateValue = moment(value, 'YYYY-MM-DDTHH:mm:ss.SSSSZ', true);
if (!dateValue.isValid()) {
if (summary) { if (summary) {
summary.isValid = false; summary.isValid = false;
summary.message = `Invalid '${column.name}' format.`; summary.message = `Invalid '${column.name}' format.`;
} }
return false; return false;
} else {
return !column.required;
} }
} }

View File

@ -17,14 +17,12 @@
import { ErrorMessageModel } from '@alfresco/adf-core'; import { ErrorMessageModel } from '@alfresco/adf-core';
/* eslint-disable @angular-eslint/component-selector */
export class DynamicRowValidationSummary extends ErrorMessageModel { export class DynamicRowValidationSummary extends ErrorMessageModel {
isValid: boolean; isValid: boolean;
constructor(json?: any) { constructor(json?: any) {
super(json); super(json);
this.isValid = json.isValid;
this.isValid = json?.isValid;
} }
} }

View File

@ -15,9 +15,6 @@
* limitations under the License. * limitations under the License.
*/ */
/* eslint-disable @angular-eslint/component-selector */
import moment from 'moment';
import { ValidateDynamicTableRowEvent } from '../../../../events/validate-dynamic-table-row.event'; import { ValidateDynamicTableRowEvent } from '../../../../events/validate-dynamic-table-row.event';
import { FormService, FormFieldModel, FormWidgetModel } from '@alfresco/adf-core'; import { FormService, FormFieldModel, FormWidgetModel } from '@alfresco/adf-core';
import { CellValidator } from './cell-validator.model'; import { CellValidator } from './cell-validator.model';
@ -178,7 +175,7 @@ export class DynamicTableModel extends FormWidgetModel {
if (column.type === 'Date') { if (column.type === 'Date') {
if (rowValue) { if (rowValue) {
return moment(rowValue.split('T')[0], 'YYYY-MM-DD').format('DD-MM-YYYY'); return new Date(rowValue.split('T')[0]);
} }
} }

View File

@ -16,7 +16,6 @@
*/ */
import { browser, protractor, ElementFinder, $$, $ } from 'protractor'; import { browser, protractor, ElementFinder, $$, $ } from 'protractor';
import { EditTaskFilterDialogPage } from './dialog/edit-task-filter-dialog.page';
import { BrowserVisibility } from '../../core/utils/browser-visibility'; import { BrowserVisibility } from '../../core/utils/browser-visibility';
import { BrowserActions } from '../../core/utils/browser-actions'; import { BrowserActions } from '../../core/utils/browser-actions';
import { DropdownPage } from '../../core/pages/material/dropdown.page'; import { DropdownPage } from '../../core/pages/material/dropdown.page';
@ -59,7 +58,6 @@ export class EditTaskFilterCloudComponentPage {
dueDateRangeDropdown = new DropdownPage($(`mat-select[data-automation-id='adf-cloud-edit-process-property-dueDateRange']`)); dueDateRangeDropdown = new DropdownPage($(`mat-select[data-automation-id='adf-cloud-edit-process-property-dueDateRange']`));
dueDateRangeWithin = new DatePickerPage($(`mat-datepicker-toggle[data-automation-id='adf-cloud-edit-picker-date-range-dueDateRange']`)); dueDateRangeWithin = new DatePickerPage($(`mat-datepicker-toggle[data-automation-id='adf-cloud-edit-picker-date-range-dueDateRange']`));
editTaskFilterDialogPage = new EditTaskFilterDialogPage();
peopleCloudComponent = new PeopleCloudComponentPage(); peopleCloudComponent = new PeopleCloudComponentPage();
groupCloudComponent = new GroupCloudComponentPage(); groupCloudComponent = new GroupCloudComponentPage();
@ -68,10 +66,6 @@ export class EditTaskFilterCloudComponentPage {
private expansionPanelExtended = this.rootElement.$('mat-expansion-panel-header.mat-expanded'); private expansionPanelExtended = this.rootElement.$('mat-expansion-panel-header.mat-expanded');
private content = this.rootElement.$('div.mat-expansion-panel-content[style*="visible"]'); private content = this.rootElement.$('div.mat-expansion-panel-content[style*="visible"]');
editTaskFilterDialog(): EditTaskFilterDialogPage {
return this.editTaskFilterDialogPage;
}
async isFilterDisplayed(): Promise<boolean> { async isFilterDisplayed(): Promise<boolean> {
return BrowserVisibility.waitUntilElementIsVisible(this.filter); return BrowserVisibility.waitUntilElementIsVisible(this.filter);
} }