Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CardView: DataController: implement Remote Operations #28843

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/devextreme/js/__internal/core/reactive/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ export function effect<T1, T2, T3, T4, T5>(
callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => ((() => void) | void),
deps: [Subscribable<T1>, Subscribable<T2>, Subscribable<T3>, Subscribable<T4>, Subscribable<T5>]
): Subscription;
export function effect<T1, T2, T3, T4, T5, T6>(
callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => ((() => void) | void),
deps: [
Subscribable<T1>,
Subscribable<T2>,
Subscribable<T3>,
Subscribable<T4>,
Subscribable<T5>,
Subscribable<T6>,
]
): Subscription;
export function effect<TArgs extends readonly any[]>(
callback: (...args: TArgs) => ((() => void) | void),
deps: { [I in keyof TArgs]: Subscribable<TArgs[I]> },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/* eslint-disable spellcheck/spell-checker */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { DataSource } from '@js/common/data';
import ArrayStore from '@js/common/data/array_store';
import type { SubsGets } from '@ts/core/reactive/index';
import {
computed, effect, state,
Expand All @@ -11,9 +12,20 @@ import { createPromise } from '@ts/core/utils/promise';

import { OptionsController } from '../options_controller/options_controller';
import type { DataObject, Key } from './types';
import { normalizeDataSource, updateItemsImmutable } from './utils';
import {
getLocalLoadOptions,
getStoreLoadOptions,
isCustomStore,
isLocalStore,
normalizeDataSource,
normalizeLocalOptions,
normalizeRemoteOptions,
updateItemsImmutable,
} from './utils';

export class DataController {
private readonly pendingLocalOperations = {};

private readonly loadedPromise = createPromise<void>();

private readonly dataSourceConfiguration = this.options.oneWay('dataSource');
Expand All @@ -34,7 +46,6 @@ export class DataController {

public readonly pageSize = this.options.twoWay('paging.pageSize');

// TODO
private readonly remoteOperations = this.options.oneWay('remoteOperations');

private readonly onDataErrorOccurred = this.options.action('onDataErrorOccurred');
Expand All @@ -54,6 +65,19 @@ export class DataController {
[this.totalCount, this.pageSize],
);

private readonly normalizedRemoteOptions = computed(
(remoteOperations, dataSource) => {
const store = dataSource.store();
return normalizeRemoteOptions(remoteOperations, isLocalStore(store), isCustomStore(store));
},
[this.remoteOperations, this.dataSource],
);

private readonly normalizedLocalOperations = computed(
(normalizedRemoteOperations) => normalizeLocalOptions(normalizedRemoteOperations),
[this.normalizedRemoteOptions],
);

public static dependencies = [OptionsController] as const;

constructor(
Expand All @@ -72,6 +96,31 @@ export class DataController {
callback({ error });
changedCallback();
};
const customizeStoreLoadOptionsCallback = (e): void => {
const localOptions = this.normalizedLocalOperations.unreactive_get();
this.pendingLocalOperations[e.operationId] = getLocalLoadOptions(
e.storeLoadOptions,
localOptions,
);
e.storeLoadOptions = getStoreLoadOptions(
e.storeLoadOptions,
localOptions,
);
};

const dataLoadedCallback = (e): void => {
/*
We use Deffered here because the code below is synchronous.
customizeLoadResult callback does not support async code.
*/
new ArrayStore(e.data).load(this.pendingLocalOperations[e.operationId]).done((data) => {
e.data = data;
}).fail((error) => {
// @ts-expect-error
e.data = new Deferred().reject(error);
});
this.pendingLocalOperations[e.operationId] = undefined;
};

if (dataSource.isLoaded()) {
changedCallback();
Expand All @@ -80,15 +129,35 @@ export class DataController {
dataSource.on('loadingChanged', loadingChangedCallback);
dataSource.on('loadError', loadErrorCallback);

// @ts-expect-error
dataSource.on('customizeStoreLoadOptions', customizeStoreLoadOptionsCallback);
// @ts-expect-error
dataSource.on('customizeLoadResult', dataLoadedCallback);

return (): void => {
dataSource.off('changed', changedCallback);
dataSource.off('loadingChanged', loadingChangedCallback);
dataSource.off('loadError', loadErrorCallback);

// @ts-expect-error
dataSource.off('customizeStoreLoadOptions', customizeStoreLoadOptionsCallback);
// @ts-expect-error
dataSource.off('customizeLoadResult', dataLoadedCallback);
};
},
[this.dataSource],
);

effect(
() => {
if (this.dataSource.unreactive_get().isLoaded()) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.dataSource.unreactive_get().load();
}
},
[this.normalizedRemoteOptions],
);

effect(
(dataSource, pageIndex, pageSize, pagingEnabled) => {
let someParamChanged = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,92 @@ describe('Options', () => {
});
});

describe.skip('remoteOperations', () => {
describe('remoteOperations', () => {
const setupForRemoteOperations = ({ remoteOperations }) => {
const store = new CustomStore({
key: 'id',
load(loadOptions) {
const data = [
{ id: 1, value: 'value 1' },
{ id: 2, value: 'value 2' },
{ id: 3, value: 'value 3' },
];

const remotePaging = loadOptions.skip === 0 && !!loadOptions.take;
if (remotePaging) {
return Promise.resolve({
data: [data[0]],
totalCount: 1,
});
}

return Promise.resolve({
data,
totalCount: data.length,
});
},
});

jest.spyOn(store, 'load');

const { dataController } = setup({
remoteOperations,
dataSource: store,
paging: {
pageSize: 1,
pageIndex: 0,
},
});

return { store, dataController };
};

it('should exclude skip and take in the store load request by default for CustomStore', async () => {
const { store, dataController } = setupForRemoteOperations({
remoteOperations: 'auto',
});

await dataController.waitLoaded();

const items = dataController.items.unreactive_get();
expect(items).toHaveLength(1);

// @ts-expect-error
expect(store.load.mock.calls[0][0].skip).toBe(undefined);
// @ts-expect-error
expect(store.load.mock.calls[0][0].take).toBe(undefined);
});

it('should exclude skip and take in the store load request if remotePaging disabled', async () => {
const { store, dataController } = setupForRemoteOperations({
remoteOperations: { paging: false },
});

await dataController.waitLoaded();

const items = dataController.items.unreactive_get();
expect(items).toHaveLength(1);

// @ts-expect-error
expect(store.load.mock.calls[0][0].skip).toBe(undefined);
// @ts-expect-error
expect(store.load.mock.calls[0][0].take).toBe(undefined);
});

it('should include skip and take in the store load request if remotePaging enabled', async () => {
const { store, dataController } = setupForRemoteOperations({
remoteOperations: { paging: true },
});

await dataController.waitLoaded();

const items = dataController.items.unreactive_get();
expect(items).toHaveLength(1);

// @ts-expect-error
expect(store.load.mock.calls[0][0].skip).toBe(0);
// @ts-expect-error
expect(store.load.mock.calls[0][0].take).toBe(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface Options {
keyExpr?: string | string[];
onDataErrorOccurred?: Action<{ error: string }>;
paging?: PagingOptions;
remoteOperations?: RemoteOperationsOptions | boolean;
remoteOperations?: RemoteOperationsOptions | boolean | 'auto';
}

export const defaultOptions = {
Expand All @@ -30,11 +30,6 @@ export const defaultOptions = {
pageSize: 6,
pageIndex: 0,
},
remoteOperations: {
filtering: false,
paging: false,
sorting: false,
summary: false,
},
remoteOperations: 'auto',
cacheEnabled: true,
} satisfies Options;
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export type DataObject = Record<string, unknown>;
export type Key = unknown;
export type KeyExpr = unknown;
export interface OperationOptions {
filtering?: boolean;
sorting?: boolean;
paging?: boolean;
}
export type RemoteOperations = boolean | OperationOptions | 'auto';
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* eslint-disable object-curly-newline */
import { describe, expect } from '@jest/globals';
import each from 'jest-each';

import {
getLocalLoadOptions, getStoreLoadOptions, normalizeLocalOptions, normalizeRemoteOptions,
} from './utils';

describe('normalizeRemoteOption', () => {
describe('with non-object arg', () => {
each`
remoteOperations | isLocalStore | isCustomStore | expectedOperationOptions
${'auto'} | ${true} | ${true} | ${{ filtering: false, sorting: false, paging: false }}
${'auto'} | ${false} | ${true} | ${{ filtering: false, sorting: false, paging: false }}
${'auto'} | ${true} | ${false} | ${{ filtering: false, sorting: false, paging: false }}
${false} | ${false} | ${false} | ${{ filtering: false, sorting: false, paging: false }}
${true} | ${false} | ${false} | ${{ filtering: true, sorting: true, paging: true }}
`
.it('should calculate the operation options', ({
remoteOperations,
isLocalStore,
isCustomStore,

expectedOperationOptions,
}) => {
const result = normalizeRemoteOptions(remoteOperations, isLocalStore, isCustomStore);
expect(result).toEqual(expectedOperationOptions);
});
});
describe('with object arg', () => {
each`
remoteOperations | isLocalStore | isCustomStore | expectedOperationOptions
${{ filtering: true, sorting: false, paging: false }} | ${true} | ${true} | ${{ filtering: true, sorting: false, paging: false }}
${{ filtering: false, sorting: true, paging: false }} | ${true} | ${true} | ${{ filtering: false, sorting: true, paging: false }}
${{ filtering: false, sorting: false, paging: true }} | ${true} | ${true} | ${{ filtering: false, sorting: false, paging: true }}
${{ filtering: false, sorting: false, paging: false }}| ${true} | ${true} | ${{ filtering: false, sorting: false, paging: false }}
`
.it('should leave the arg as is', ({
remoteOperations,
isLocalStore,
isCustomStore,

expectedOperationOptions,
}) => {
const result = normalizeRemoteOptions(remoteOperations, isLocalStore, isCustomStore);
expect(result).toEqual(expectedOperationOptions);
});
});
});

describe('normalizeLocalOption', () => {
each`
remoteOperations | expectedOperationOptions
${{ filtering: true, sorting: false, paging: false }} | ${{ filtering: false, sorting: true, paging: true }}
${{ filtering: false, sorting: true, paging: false }} | ${{ filtering: true, sorting: false, paging: true }}
${{ filtering: false, sorting: false, paging: true }} | ${{ filtering: true, sorting: true, paging: false }}

${{ filtering: true, sorting: true, paging: true }} | ${{ filtering: false, sorting: false, paging: false }}
${{ filtering: false, sorting: false, paging: false }}| ${{ filtering: true, sorting: true, paging: true }}
`
.it('should invert remoteOperations', ({
remoteOperations,
expectedOperationOptions,
}) => {
const result = normalizeLocalOptions(remoteOperations);
expect(result).toEqual(expectedOperationOptions);
});
});

describe('getLocalLoadOptions', () => {
each`
originOptions | localOperations | expectedLoadOptions
${{ filter: 'test', sort: 'asc', skip: 0, take: 20 }} | ${{ filtering: true }} | ${{ filter: 'test' }}
${{ filter: 'test', sort: 'asc', skip: 0, take: 20 }} | ${{ sorting: true }} | ${{ sort: 'asc' }}
${{ filter: 'test', sort: 'asc', skip: 0, take: 20 }} | ${{ paging: true }} | ${{ skip: 0, take: 20 }}
`
.it('should convert local operation to load options', ({
originOptions,
localOperations,

expectedLoadOptions,
}) => {
const result = getLocalLoadOptions(originOptions, localOperations);
expect(result).toEqual(expectedLoadOptions);
});
});

describe('getStoreLoadOptions', () => {
each`
originOptions | localOperations | expectedLoadOptions
${{ filter: 'test', sort: 'asc', skip: 0, take: 20 }} | ${{ filtering: true }} | ${{ sort: 'asc', skip: 0, take: 20 }}
${{ filter: 'test', sort: 'asc', skip: 0, take: 20 }} | ${{ sorting: true }} | ${{ filter: 'test', skip: 0, take: 20 }}
${{ filter: 'test', sort: 'asc', skip: 0, take: 20 }} | ${{ paging: true }} | ${{ filter: 'test', sort: 'asc' }}
`
.it('should clear local operations from load options', ({
originOptions,
localOperations,

expectedLoadOptions,
}) => {
const result = getStoreLoadOptions(originOptions, localOperations);
expect(result).toEqual(expectedLoadOptions);
});
});
Loading
Loading