mattermost-mobile-upstream/app/components/channel_attribute_editor/utils.ts
Pablo Vélez 4d842026f6
Some checks failed
ci / lint-typecheck (push) Has been cancelled
ci / detox-lint-typecheck (push) Has been cancelled
ci / test (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Mm 70564 channel labels use in channel creation (#10146)
* 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>
2026-10-30 15:03:55 +01:00

105 lines
4.9 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BUTTON_HEIGHT} from '@screens/bottom_sheet/button';
import {TITLE_HEIGHT, TITLE_LINE_HEIGHT, TITLE_SEPARATOR_MARGIN, TITLE_SEPARATOR_MARGIN_TABLET} from '@screens/bottom_sheet/content';
import {bottomSheetSnapPoint} from '@utils/helpers';
import type {ResolvedChannelAttribute} from '@utils/channel_attributes';
export const OPTION_ROW_HEIGHT = 48;
export const TEXT_INPUT_MARGIN_BOTTOM = 12;
// FloatingTextInput's own rendered box while focused: DEFAULT_INPUT_HEIGHT (48)
// plus its focused 2px border on both edges. The field autoFocuses on open, so
// this is the height it actually renders at, not its unfocused 1px-border size.
const TEXT_INPUT_HEIGHT = 52;
const TITLE_SEPARATOR_LINE_HEIGHT = 1;
// A fixed cushion under Save, on top of BottomSheetButton's own safe-area
// padding: Android reports that inset as 0 while a keyboard is up (real or,
// with a hardware/passthrough keyboard, merely believed to be), which the text
// editor's autofocused input triggers on open and the option editors never do.
export const SAVE_BUTTON_BOTTOM_PADDING = 12;
// Past this many rows the sheet opens at a fraction of the window rather than
// growing to fit every row, and the rest is reached by scrolling.
export const SHEET_MAX_ROWS = 5;
// The sheet never opens taller than this fraction of the window, matching the
// '80%' ceiling every other sheet in the app already opens at.
const MAX_SHEET_HEIGHT_RATIO = 0.8;
type FieldType = ResolvedChannelAttribute['field']['type'];
/**
* Snap points for the Set {attribute} sheet, ordered bottom to top as Gorhom
* requires.
*
* The numeric point is a starting estimate, not a guarantee: a long option name
* or a wrapped title can still make the real content taller than this predicts.
* That is fine because the sheet's own content — title included — is rendered
* inside a scroll area, and Save is laid out in its own row after that area
* rather than inside it: an underestimate here costs a scroll, never a clipped
* Save button. The estimate only exists so the sheet opens at roughly the right
* height instead of always starting at the tallest point allowed.
*
* totalRows is the full, unclamped row count so the caller does not have to
* duplicate the SHEET_MAX_ROWS comparison to know whether a taller snap point
* is worth offering.
*/
export function getChannelAttributeEditorSnapPoints({
totalRows,
fieldType,
bottomInset,
isTablet,
windowHeight,
}: {
totalRows: number;
fieldType: FieldType;
bottomInset: number;
isTablet: boolean;
windowHeight: number;
}): number[] {
const hasSaveButton = fieldType === 'text' || fieldType === 'multiselect';
const separatorMargin = isTablet ? TITLE_SEPARATOR_MARGIN_TABLET : TITLE_SEPARATOR_MARGIN;
const rowsHeight = fieldType === 'text' ? TEXT_INPUT_HEIGHT + TEXT_INPUT_MARGIN_BOTTOM : bottomSheetSnapPoint(Math.min(totalRows, SHEET_MAX_ROWS), OPTION_ROW_HEIGHT);
const estimatedHeight = rowsHeight +
TITLE_HEIGHT + TITLE_LINE_HEIGHT + separatorMargin + TITLE_SEPARATOR_LINE_HEIGHT +
// BUTTON_HEIGHT does not include the button's own bottom safe-area padding
// (see screens/bottom_sheet/button.tsx), so that inset is added here too.
(hasSaveButton ? BUTTON_HEIGHT + bottomInset + SAVE_BUTTON_BOTTOM_PADDING : 0);
const maxHeight = windowHeight * MAX_SHEET_HEIGHT_RATIO;
// GenericBottomSheetRoute adds this same margin (the safe-area bottom inset)
// to index 1 (and only index 1) after this function returns, to give every
// sheet a bottom cushion. A third point too close to the second would end up
// smaller than the second once that addition lands, leaving Gorhom an
// unordered array — so a third point is only offered when it clears the
// second by more than that.
const routeMargin = bottomInset;
// Capped at maxHeight - routeMargin, not maxHeight itself: the route adds
// routeMargin to this point after this function returns, so capping at
// maxHeight here would let the effective, on-screen height exceed the 80%
// ceiling by up to that margin.
const collapsedHeight = Math.min(estimatedHeight, maxHeight - routeMargin);
// A taller point is only worth offering when there are more rows than the
// collapsed estimate already accounts for. A text field never has more than
// one row and never needs one for keyboard purposes either: the caller
// opens it with keyboardBehavior="interactive", which grows the sheet by
// exactly the keyboard's height instead of jumping to a configured snap
// point, so there is nothing here for a keyboard to need room to extend to.
const rowsOverflow = fieldType !== 'text' && totalRows > SHEET_MAX_ROWS;
if (!rowsOverflow || collapsedHeight + routeMargin >= maxHeight) {
return [1, collapsedHeight];
}
return [1, collapsedHeight, maxHeight];
}