mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-04 18:08:54 +00:00
* MM-70516 - Add ability to edit channel attribute values from Channel Info * Add API methods for patching property fields and setting channel attribute requirements * Update channel attribute clearing behavior for required and non-required fields * MM-70564 - use channel attributes on channel creation * Fix undefined propertyValues and ternary formatting in channel attributes * Fix spelling of "colour" to "color" in attribute chip tests and related comments * Add visibility checks for channel attribute labels in tests * Fix data-integrity, permission, and accessibility issues in channel attributes: scoped pruning, atomic writes, race serialization, required clear/stale-option guards * Fix handling of null values in apiGetChannelAttributeValue and update test assertions for channel attributes * Improve channel attribute editor sizing for long labels, keep Save visible, and prevent keyboard overlap on Android * Address CodeRabbit feedback for nullable values, bottom-sheet state, async validation, and reliable E2E cleanup * lock-safe field fetch, create-time gate recheck, scoped value pruning, and tappable header chips * Address CodeRabbit feedback: drop queued attribute saves on channel switch and use page object selectors in E2E * Respect the ChannelAttributesRequired flag on channel creation and enable it for channel attribute E2E * Skip required channel attribute E2E tests and stop enabling ChannelAttributesRequired on the shared server until they move to SITE_3 * Fix clearing channel attributes on iOS by sending an empty value instead of null, and stop E2E suites from turning off the ChannelAttributes flag mid-run * Refactor getEmptyPropertyValue to accept string type for fieldType parameter * E2E: give channel attribute test fields a suite-unique suffix Pool shards may only create optional property fields named <name>_<6 hex>; fixed names were blocked by the pool guard. * E2E: restore channel attribute cleanup checks; name network error codes Brings back the PR's afterEach/afterAll error handling that the main merge replaced, and makes a no-response error show its codes (an AggregateError has an empty message). * E2E: run channel attribute creation and editing specs on server B Their 11 required-attribute tests could not run on the shared servers. Both specs now run on the mutation server with ChannelAttributesRequired on for their run, and the tests are re-enabled. setChannelAttributesRequired moves to the shared helper. * Check header chips instead of their wrapper and use toExist for the create screen in channel attribute E2E --------- Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
206 lines
7.5 KiB
TypeScript
206 lines
7.5 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {router} from 'expo-router';
|
|
import {DeviceEventEmitter} from 'react-native';
|
|
|
|
import {Events, Navigation, Screens} from '@constants';
|
|
import {UNAUTHENTICATED_SCREENS, HOME_TAB_SCREENS, SCREENS_AS_BOTTOM_SHEET, MODAL_SCREENS} from '@constants/screens';
|
|
import BottomSheetStore from '@store/bottom_sheet_store';
|
|
import {NavigationStore} from '@store/navigation_store';
|
|
import {logError} from '@utils/log';
|
|
|
|
import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
|
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
|
|
|
// Expo Router URI-decodes params more than once between push and useLocalSearchParams, so any
|
|
// "%XX" in a value would be decoded. Escaping "%" as a JSON unicode escape leaves nothing to decode.
|
|
export function propsToParams(props: any): Record<string, string> {
|
|
return Object.keys(props || {}).reduce((params, key) => {
|
|
params[key] = JSON.stringify(props[key])?.replace(/%/g, '\\u0025');
|
|
return params;
|
|
}, {} as Record<string, string>);
|
|
}
|
|
|
|
export function updateParams(props: Record<string, any>) {
|
|
if (router) {
|
|
const params = propsToParams(props);
|
|
router.setParams(params);
|
|
}
|
|
}
|
|
|
|
// Helper to build Expo Router path from screen constant
|
|
export function getExpoRouterPath(screen: AvailableScreens, props?: any): string | undefined {
|
|
if (UNAUTHENTICATED_SCREENS.has(screen)) {
|
|
if (props?.isModal) {
|
|
return `/(modals)/(add-server)/${screen}`;
|
|
}
|
|
return `/(unauthenticated)/${screen}`;
|
|
}
|
|
if (HOME_TAB_SCREENS.has(screen)) {
|
|
return `/(authenticated)/(home)/${screen}`;
|
|
}
|
|
|
|
if (SCREENS_AS_BOTTOM_SHEET.has(screen)) {
|
|
return `/(bottom_sheet)/${screen}`;
|
|
}
|
|
|
|
if (MODAL_SCREENS.has(screen)) {
|
|
return `/(modals)/${screen}`;
|
|
}
|
|
|
|
return `/(authenticated)/${screen}`;
|
|
}
|
|
|
|
/**
|
|
* Navigation adapter that works with both RNN and Expo Router
|
|
* Tries Expo Router first, falls back to RNN if not available
|
|
*
|
|
* @param screen - Screen constant from @constants
|
|
* @param title - Screen title (used by RNN, ignored by Expo Router)
|
|
* @param params - Route parameters including theme
|
|
* @param options - RNN layout options (ignored by Expo Router as theme comes from params)
|
|
*/
|
|
export function navigateToScreen(screen: AvailableScreens, props?: Record<string, unknown>, reset = false) {
|
|
try {
|
|
if (router) {
|
|
const route = getExpoRouterPath(screen, props);
|
|
if (route) {
|
|
const params = propsToParams(props);
|
|
if (reset) {
|
|
router.replace({pathname: route, params});
|
|
} else {
|
|
router.push({pathname: route, params});
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logError('navigateToScreen: Expo Router navigation failed', e);
|
|
}
|
|
}
|
|
|
|
export function navigateToScreenWithBaseRoute(baseRoute: string, screen: AvailableScreens, props?: Record<string, unknown>, reset = false) {
|
|
try {
|
|
const pathname = `${baseRoute}/${screen}`;
|
|
const params = propsToParams(props);
|
|
if (reset) {
|
|
router.replace({pathname, params});
|
|
} else {
|
|
router.push({pathname, params});
|
|
}
|
|
} catch (e) {
|
|
logError('navigateToScreenWithBaseRoute: Expo Router navigation failed', e);
|
|
}
|
|
}
|
|
|
|
export async function navigateBack() {
|
|
if (router && router.canGoBack()) {
|
|
router.back();
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
}
|
|
|
|
export function navigateToHomeTab(params?: Record<string, unknown>) {
|
|
DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, {
|
|
screen: Screens.CHANNEL_LIST,
|
|
params,
|
|
});
|
|
}
|
|
|
|
export async function dismissToStackRoot() {
|
|
if (router && router.canDismiss()) {
|
|
router.dismissAll();
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
}
|
|
|
|
export function bottomSheet(
|
|
renderContent: () => React.ReactNode,
|
|
snapPoints: Array<string | number>,
|
|
footerComponent?: (props: BottomSheetFooterProps) => React.ReactNode,
|
|
keyboardBehavior?: 'extend' | 'fillParent' | 'interactive',
|
|
) {
|
|
DeviceEventEmitter.emit(Events.BLUR_AND_DISMISS_KEYBOARD);
|
|
BottomSheetStore.setSnapPoints(snapPoints);
|
|
BottomSheetStore.setRenderContentCallback(renderContent);
|
|
if (footerComponent) {
|
|
BottomSheetStore.setFooterComponent(footerComponent);
|
|
} else {
|
|
BottomSheetStore.removeFooterComponent();
|
|
}
|
|
if (keyboardBehavior) {
|
|
BottomSheetStore.setKeyboardBehavior(keyboardBehavior);
|
|
} else {
|
|
BottomSheetStore.removeKeyboardBehavior();
|
|
}
|
|
|
|
navigateToScreen(Screens.GENERIC_BOTTOM_SHEET);
|
|
}
|
|
|
|
export async function dismissBottomSheet() {
|
|
const hasRegularSheet = NavigationStore.isScreenInStack(Screens.BOTTOM_SHEET);
|
|
const hasGenericSheet = NavigationStore.isScreenInStack(Screens.GENERIC_BOTTOM_SHEET);
|
|
if (!hasRegularSheet && !hasGenericSheet) {
|
|
return;
|
|
}
|
|
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
|
|
|
// Prefer the regular BOTTOM_SHEET when present (historical contract); fall back to
|
|
// GENERIC_BOTTOM_SHEET so callers using the generic variant aren't silently ignored.
|
|
await NavigationStore.waitUntilScreensIsRemoved(hasRegularSheet ? Screens.BOTTOM_SHEET : Screens.GENERIC_BOTTOM_SHEET);
|
|
BottomSheetStore.reset();
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
|
|
// Dismiss all screens (modals, bottom sheets, stack pushes) and return to channel_list.
|
|
// router.dismissTo traverses all navigator boundaries in expo-router 55, so a single call
|
|
// handles any stack shape: bottom sheet on top of modals on top of authenticated stack pushes.
|
|
export async function navigateToRoot() {
|
|
if (router) {
|
|
router.dismissTo(getExpoRouterPath(Screens.CHANNEL_LIST)!);
|
|
|
|
if (router.canDismiss()) {
|
|
router.dismissAll();
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
}
|
|
|
|
export async function dismissAllRoutesAndPopToScreen(screenId: AvailableScreens, passProps = {}) {
|
|
try {
|
|
if (!router) {
|
|
return;
|
|
}
|
|
const route = getExpoRouterPath(screenId);
|
|
if (!route) {
|
|
return;
|
|
}
|
|
|
|
if (NavigationStore.isScreenInStack(screenId)) {
|
|
// dismissTo only resolves divergence at the outermost navigator level
|
|
// it finds. With our nesting (root Stack -> (authenticated) Stack), one
|
|
// call only peels outer routes (modals, bottom sheets). A second call
|
|
// then operates on the inner stack and pops down to the target.
|
|
router.dismissTo(route);
|
|
router.dismissTo(route);
|
|
router.setParams(propsToParams(passProps));
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
} else {
|
|
// Screen not in stack - reset to root then push target
|
|
await navigateToRoot();
|
|
navigateToScreen(screenId, passProps);
|
|
}
|
|
} catch (e) {
|
|
logError('dismissAllRoutesAndPopToScreen: Expo Router navigation failed', e);
|
|
}
|
|
}
|
|
|
|
export function navigateToSettingsScreen(screen: AvailableScreens, props?: Record<string, unknown>) {
|
|
navigateToScreenWithBaseRoute(`/(modals)/${Screens.SETTINGS}`, screen, props);
|
|
}
|
|
|
|
export function navigateToChannelInfoScreen(screen: AvailableScreens, props?: Record<string, unknown>) {
|
|
navigateToScreenWithBaseRoute(`/(modals)/${Screens.CHANNEL_INFO}`, screen, props);
|
|
}
|