UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

476 lines 18.5 kB
"use strict"; /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.computeAddRangeToSelection = exports.computeSetSelection = exports.safeCreateRegExp = exports.computeDataTableFilter = exports.getValueAtPath = exports.savePreferences = exports.getSelectedItems = exports.getSelectedItem = exports.createInitialState = exports.createDataTableManager = exports.dataTableManagerReducer = void 0; const immer_1 = __importStar(require("immer")); const DataTableDefaultPowerSearchOperators_1 = require("./DataTableDefaultPowerSearchOperators"); const FlipperLib_1 = require("../../plugin/FlipperLib"); const emptySelection = { items: new Set(), current: -1, }; exports.dataTableManagerReducer = (0, immer_1.default)(function (draft, action) { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const config = (0, immer_1.original)(draft.config); switch (action.type) { case 'reset': { draft.columns = computeInitialColumns(config.defaultColumns); draft.sorting = undefined; draft.searchExpression = []; draft.selection = (0, immer_1.castDraft)(emptySelection); draft.filterExceptions = undefined; break; } case 'resetFilters': { draft.searchExpression = []; draft.filterExceptions = undefined; break; } case 'resizeColumn': { const { column, width } = action; // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const col = draft.columns.find((c) => c.key === column); col.width = width; break; } case 'sortColumn': { const { column, direction } = action; if (direction === undefined) { draft.sorting = undefined; } else { draft.sorting = { key: column, direction }; } break; } case 'toggleColumnVisibility': { const { column } = action; // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const col = draft.columns.find((c) => c.key === column); col.visible = !col.visible; break; } case 'setSearchExpression': { (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:power-search'); draft.searchExpression = action.searchExpression ?? []; draft.filterExceptions = undefined; break; } case 'setSearchExpressionFromSelection': { (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:power-search-from-selection'); draft.filterExceptions = undefined; const items = getSelectedItems(config.dataView, draft.selection); const searchExpressionFromSelection = [ { field: { key: action.column.key, label: action.column.title ?? action.column.key, }, operator: DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.enum_set_is_any_of({}), searchValue: items.map((item) => getValueAtPath(item, action.column.key)), }, ]; draft.searchExpression = searchExpressionFromSelection; draft.filterExceptions = undefined; break; } case 'selectItem': { const { nextIndex, addToSelection, allowUnselect } = action; draft.selection = (0, immer_1.castDraft)(computeSetSelection(draft.selection, nextIndex, addToSelection, allowUnselect)); break; } case 'selectItemById': { const { id, addToSelection } = action; // TODO: fix that this doesn't jumpt selection if items are shifted! sorting is swapped etc const idx = config.dataSource.getIndexOfKey(id); if (idx !== -1) { draft.selection = (0, immer_1.castDraft)(computeSetSelection(draft.selection, idx, addToSelection)); } break; } case 'addRangeToSelection': { const { start, end, allowUnselect } = action; draft.selection = (0, immer_1.castDraft)(computeAddRangeToSelection(draft.selection, start, end, allowUnselect)); break; } case 'clearSelection': { draft.selection = (0, immer_1.castDraft)(emptySelection); break; } case 'appliedInitialScroll': { draft.initialOffset = 0; break; } case 'toggleAutoScroll': { draft.autoScroll = !draft.autoScroll; break; } case 'setAutoScroll': { draft.autoScroll = action.autoScroll; break; } case 'toggleSideBySide': { draft.sideBySide = !draft.sideBySide; break; } case 'setFilterExceptions': { draft.filterExceptions = action.exceptions; break; } default: { throw new Error(`Unknown action ${action.type}`); } } }); const showPowerSearchMigrationWarning = () => { console.warn('Flipper is migrating to the new power search (see https://fburl.com/workplace/eewxik3o). Your plugin uses tableManagerRef which is partially incompatible with the new API. THIS API CALL DOES NOTHING AT THIS POINT! Please, migrate to the new API by explicitly using _MasterDetailWithPowerSearch, _DataTableWithPowerSearch, _DataTableWithPowerSearchManager (see https://fburl.com/code/dpawdt69). As a temporary workaround, feel free to use legacy MasterDetailLegacy, DataTableLegacy, DataTableManagerLegacy components to force the usage of the old search.'); (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:power-search-legacy-api-access'); }; function createDataTableManager(dataView, dispatch, stateRef) { return { reset() { dispatch({ type: 'reset' }); }, resetFilters() { dispatch({ type: 'resetFilters' }); }, setAutoScroll(autoScroll) { dispatch({ type: 'setAutoScroll', autoScroll }); }, selectItem(index, addToSelection = false, allowUnselect = false) { dispatch({ type: 'selectItem', nextIndex: index, addToSelection, allowUnselect, }); }, selectItemById(id, addToSelection = false) { dispatch({ type: 'selectItemById', id, addToSelection }); }, addRangeToSelection(start, end, allowUnselect = false) { dispatch({ type: 'addRangeToSelection', start, end, allowUnselect }); }, clearSelection() { dispatch({ type: 'clearSelection' }); }, getSelectedItem() { return getSelectedItem(dataView, stateRef.current.selection); }, getSelectedItems() { return getSelectedItems(dataView, stateRef.current.selection); }, toggleColumnVisibility(column) { dispatch({ type: 'toggleColumnVisibility', column }); }, sortColumn(column, direction) { dispatch({ type: 'sortColumn', column, direction }); }, setSearchExpression(searchExpression) { dispatch({ type: 'setSearchExpression', searchExpression }); }, toggleSideBySide() { dispatch({ type: 'toggleSideBySide' }); }, setFilterExceptions(exceptions) { dispatch({ type: 'setFilterExceptions', exceptions }); }, dataView, stateRef, showSearchDropdown() { showPowerSearchMigrationWarning(); }, setSearchValue() { showPowerSearchMigrationWarning(); }, setSearchHighlightColor() { showPowerSearchMigrationWarning(); }, setShowNumberedHistory() { showPowerSearchMigrationWarning(); }, toggleSearchValue() { showPowerSearchMigrationWarning(); }, toggleHighlightSearch() { showPowerSearchMigrationWarning(); }, addColumnFilter() { showPowerSearchMigrationWarning(); }, removeColumnFilter() { showPowerSearchMigrationWarning(); }, }; } exports.createDataTableManager = createDataTableManager; function createInitialState(config) { // by default a table is considered to be identical if plugins, and default column names are the same const storageKey = `${config.scope}:DataTable:${config.defaultColumns .map((c) => c.key) .join(',')}`; const prefs = config.enablePersistSettings ? loadStateFromStorage(storageKey) : undefined; let initialColumns = computeInitialColumns(config.defaultColumns); if (prefs) { // merge prefs with the default column config initialColumns = (0, immer_1.default)(initialColumns, (draft) => { prefs.columns.forEach((pref) => { const existing = draft.find((c) => c.key === pref.key); if (existing) { Object.assign(existing, pref); } }); }); } let searchExpression = config.initialSearchExpression ?? []; if (prefs?.searchExpression?.length) { searchExpression = prefs.searchExpression; } const res = { config, storageKey, initialOffset: prefs?.scrollOffset ?? 0, usesWrapping: config.defaultColumns.some((col) => col.wrap), columns: initialColumns, sorting: prefs?.sorting, selection: prefs?.selection ? { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion current: prefs.selection.current, // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion items: new Set(prefs.selection.items), } : emptySelection, searchExpression, filterExceptions: undefined, autoScroll: prefs?.autoScroll ?? config.autoScroll ?? false, sideBySide: false, }; // @ts-ignore res.config[immer_1.immerable] = false; // optimization: never proxy anything in config Object.freeze(res.config); return res; } exports.createInitialState = createInitialState; function getSelectedItem(dataView, selection) { return selection.current < 0 ? undefined : dataView.get(selection.current); } exports.getSelectedItem = getSelectedItem; function getSelectedItems(dataView, selection) { return [...selection.items] .sort((a, b) => a - b) // https://stackoverflow.com/a/15765283/1983583 .map((i) => dataView.get(i)) .filter(Boolean); } exports.getSelectedItems = getSelectedItems; function savePreferences(state, scrollOffset) { if (!state.config.scope || !state.config.enablePersistSettings) { return; } const prefs = { searchExpression: state.searchExpression, selection: { current: state.selection.current, items: Array.from(state.selection.items), }, sorting: state.sorting, columns: state.columns.map((c) => ({ key: c.key, width: c.width, visible: c.visible, })), scrollOffset, autoScroll: state.autoScroll, }; localStorage.setItem(state.storageKey, JSON.stringify(prefs)); } exports.savePreferences = savePreferences; function loadStateFromStorage(storageKey) { if (!storageKey) { return undefined; } const state = localStorage.getItem(storageKey); if (!state) { return undefined; } try { return JSON.parse(state); } catch (e) { // forget about this state return undefined; } } function computeInitialColumns(columns) { const visibleColumnCount = columns.filter((c) => c.visible !== false).length; const columnsWithoutWidth = columns.filter((c) => c.visible !== false && c.width === undefined).length; return columns.map((c) => ({ ...c, width: c.width ?? // if the width is not set, and there are multiple columns with unset widths, // there will be multiple columns ith the same flex weight (1), meaning that // they will all resize a best fits in a specifc row. // To address that we distribute space equally // (this need further fine tuning in the future as with a subset of fixed columns width can become >100%) (columnsWithoutWidth > 1 ? `${Math.floor(100 / visibleColumnCount)}%` : undefined), visible: c.visible !== false, })); } /** * A somewhat primitive and unsafe way to access nested fields an object. * @param obj keys should only be strings * @param keyPath dotted string path, e.g foo.bar * @returns value at the key path */ function getValueAtPath(obj, keyPath) { let res = obj; for (const key of keyPath.split('.')) { if (res == null) { return null; } else { res = res[key]; } } return res; } exports.getValueAtPath = getValueAtPath; function computeDataTableFilter(searchExpression, powerSearchProcessors, treatUndefinedValuesAsMatchingFiltering = false) { return function dataTableFilter(item) { if (!searchExpression.length) { return true; } return searchExpression.every((searchTerm) => { const value = searchTerm.field.useWholeRow ? item : getValueAtPath(item, searchTerm.field.key); const processor = powerSearchProcessors[searchTerm.operator.key]; if (!processor) { console.warn('computeDataTableFilter -> processor at searchTerm.operator.key is not recognized', searchTerm, powerSearchProcessors); return true; } try { const res = processor(searchTerm.operator, searchTerm.searchValue, value); if (!res && !value) { return treatUndefinedValuesAsMatchingFiltering; } return res; } catch { return treatUndefinedValuesAsMatchingFiltering; } }); }; } exports.computeDataTableFilter = computeDataTableFilter; function safeCreateRegExp(source) { try { return new RegExp(source); } catch (_e) { return undefined; } } exports.safeCreateRegExp = safeCreateRegExp; function computeSetSelection(base, nextIndex, addToSelection, allowUnselect) { const newIndex = typeof nextIndex === 'number' ? nextIndex : nextIndex(base.current); // special case: toggle existing selection off if (!addToSelection && allowUnselect && base.items.size === 1 && base.current === newIndex) { return emptySelection; } if (newIndex < 0) { return emptySelection; } if (base.current < 0 || !addToSelection) { return { current: newIndex, items: new Set([newIndex]), }; } else { const lowest = Math.min(base.current, newIndex); const highest = Math.max(base.current, newIndex); return { current: newIndex, items: addIndicesToMultiSelection(base.items, lowest, highest), }; } } exports.computeSetSelection = computeSetSelection; function computeAddRangeToSelection(base, start, end, allowUnselect) { // special case: unselectiong a single item with the selection if (start === end && allowUnselect) { if (base?.items.has(start)) { const copy = new Set(base.items); copy.delete(start); const current = [...copy]; if (current.length === 0) { return emptySelection; } return { items: copy, current: current[current.length - 1], // back to the last selected one }; } // intentional fall-through } // N.B. start and end can be reverted if selecting backwards const lowest = Math.min(start, end); const highest = Math.max(start, end); const current = end; return { items: addIndicesToMultiSelection(base.items, lowest, highest), current, }; } exports.computeAddRangeToSelection = computeAddRangeToSelection; function addIndicesToMultiSelection(base, lowest, highest) { const copy = new Set(base); for (let i = lowest; i <= highest; i++) { copy.add(i); } return copy; } //# sourceMappingURL=DataTableWithPowerSearchManager.js.map