UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

636 lines 24.9 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 theme_1 = require("../theme"); const FlipperLib_1 = require("../../plugin/FlipperLib"); const emptySelection = { items: new Set(), current: -1, }; const MAX_HISTORY = 1000; 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.searchValue = ''; draft.selection = (0, immer_1.castDraft)(emptySelection); draft.filterExceptions = undefined; break; } case 'resetFilters': { draft.columns.forEach((c) => c.filters?.forEach((f) => (f.enabled = false))); draft.searchValue = ''; 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 'setSearchValue': { (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:search'); draft.searchValue = action.value; draft.previousSearchValue = ''; draft.filterExceptions = undefined; if (action.addToHistory && action.value && !draft.searchHistory.includes(action.value)) { draft.searchHistory.unshift(action.value); // FIFO if history too large if (draft.searchHistory.length > MAX_HISTORY) { draft.searchHistory.length = MAX_HISTORY; } } break; } case 'toggleSearchValue': { (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:toggle-search'); draft.filterExceptions = undefined; if (draft.searchValue) { draft.previousSearchValue = draft.searchValue; draft.searchValue = ''; } else { draft.searchValue = draft.previousSearchValue; draft.previousSearchValue = ''; } break; } case 'clearSearchHistory': { draft.searchHistory = []; break; } case 'toggleUseRegex': { draft.useRegex = !draft.useRegex; break; } case 'toggleFilterSearchHistory': { draft.filterSearchHistory = !draft.filterSearchHistory; 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 'addColumnFilter': { (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:add-column'); draft.filterExceptions = undefined; addColumnFilter(draft.columns, action.column, action.value, action.options); break; } case 'removeColumnFilter': { (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:remove-column'); draft.filterExceptions = undefined; // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const column = draft.columns.find((c) => c.key === action.column); const index = action.index ?? // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion column.filters?.findIndex((f) => f.label === action.label); if (index === undefined || index < 0) { break; } column.filters?.splice(index, 1); break; } case 'toggleColumnFilter': { (0, FlipperLib_1.getFlipperLib)().logger.track('usage', 'data-table:filter:toggle-column'); draft.filterExceptions = undefined; // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const column = draft.columns.find((c) => c.key === action.column); const index = action.index ?? // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion column.filters?.findIndex((f) => f.label === action.label); if (index === undefined || index < 0) { break; } // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const f = column.filters[index]; f.enabled = !f.enabled; break; } case 'setColumnFilterInverse': { draft.filterExceptions = undefined; // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion draft.columns.find((c) => c.key === action.column).inversed = action.inversed; break; } case 'setColumnFilterFromSelection': { draft.filterExceptions = undefined; const items = getSelectedItems(config.dataView, draft.selection); items.forEach((item, index) => { addColumnFilter(draft.columns, action.column, getValueAtPath(item, String(action.column)), { disableOthers: index === 0, // remove existing filters before adding the first exact: true, }); }); break; } case 'appliedInitialScroll': { draft.initialOffset = 0; break; } case 'toggleAutoScroll': { draft.autoScroll = !draft.autoScroll; break; } case 'setAutoScroll': { draft.autoScroll = action.autoScroll; break; } case 'toggleHighlightSearch': { draft.highlightSearchSetting.highlightEnabled = !draft.highlightSearchSetting.highlightEnabled; break; } case 'setSearchHighlightColor': { if (draft.highlightSearchSetting.color !== action.color) { draft.highlightSearchSetting.color = action.color; } break; } case 'toggleSideBySide': { draft.sideBySide = !draft.sideBySide; break; } case 'showSearchDropdown': { draft.showSearchHistory = action.show; break; } case 'setShowNumberedHistory': { draft.showNumberedHistory = action.showNumberedHistory; break; } case 'setFilterExceptions': { draft.filterExceptions = action.exceptions; break; } default: { throw new Error(`Unknown action ${action.type}`); } } }); 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 }); }, setSearchValue(value, addToHistory = false) { dispatch({ type: 'setSearchValue', value, addToHistory }); }, toggleSearchValue() { dispatch({ type: 'toggleSearchValue' }); }, toggleHighlightSearch() { dispatch({ type: 'toggleHighlightSearch' }); }, setSearchHighlightColor(color) { dispatch({ type: 'setSearchHighlightColor', color }); }, toggleSideBySide() { dispatch({ type: 'toggleSideBySide' }); }, showSearchDropdown(show) { dispatch({ type: 'showSearchDropdown', show }); }, setShowNumberedHistory(showNumberedHistory) { dispatch({ type: 'setShowNumberedHistory', showNumberedHistory }); }, addColumnFilter(column, value, options = {}) { dispatch({ type: 'addColumnFilter', column, value, options }); }, removeColumnFilter(column, label) { dispatch({ type: 'removeColumnFilter', column, label }); }, setFilterExceptions(exceptions) { dispatch({ type: 'setFilterExceptions', exceptions }); }, dataView, stateRef, }; } 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); } }); }); } 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, searchValue: prefs?.search ?? '', previousSearchValue: '', searchHistory: prefs?.searchHistory ?? [], useRegex: prefs?.useRegex ?? false, filterSearchHistory: prefs?.filterSearchHistory ?? true, filterExceptions: undefined, autoScroll: prefs?.autoScroll ?? config.autoScroll ?? false, highlightSearchSetting: prefs?.highlightSearchSetting ?? { highlightEnabled: false, color: theme_1.theme.searchHighlightBackground.yellow, }, sideBySide: false, showSearchHistory: false, showNumberedHistory: 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 addColumnFilter(columns, columnId, value, options = {}) { options = Object.assign({ disableOthers: false, strict: true }, options); // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const column = columns.find((c) => c.key === columnId); const filterValue = options.exact ? String(value) : String(value).toLowerCase(); // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const existing = column.filters.find((c) => c.value === filterValue); if (existing) { existing.enabled = true; } else { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion column.filters.push({ label: String(value), value: filterValue, enabled: true, strict: options.strict, exact: options.exact, }); } if (options.disableOthers) { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion column.filters.forEach((c) => { if (c.value !== filterValue) { c.enabled = false; } }); } } 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 = { search: state.searchValue, useRegex: state.useRegex, filterSearchHistory: state.filterSearchHistory, 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, filters: c.filters, visible: c.visible, inversed: c.inversed, })), scrollOffset, autoScroll: state.autoScroll, searchHistory: state.searchHistory, highlightSearchSetting: state.highlightSearchSetting, }; 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), filters: c.filters?.map((f) => ({ ...f, predefined: true, })) ?? [], 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(searchValue, useRegex, columns) { const searchString = searchValue.toLowerCase(); const searchRegex = useRegex ? safeCreateRegExp(searchValue) : undefined; // the columns with an active filter are those that have filters defined, // with at least one enabled const filteringColumns = columns.filter((c) => c.filters?.some((f) => f.enabled)); if (searchValue === '' && !filteringColumns.length) { // unset return undefined; } return function dataTableFilter(item) { for (const column of filteringColumns) { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const rowMatchesFilter = column.filters.some((f) => { if (!f.enabled) { return false; } const value = getValueAtPath(item, column.key); const isMatching = f.exact ? String(value) === f.value : String(value).toLowerCase().includes(f.value); return f.strict ? isMatching : isMatching || value === undefined; }); if (column.inversed && rowMatchesFilter) { return false; } if (!column.inversed && !rowMatchesFilter) { return false; } } //free search all top level keys as well as any (nested) columns in the table const nestedColumns = columns .map((col) => col.key) .filter((path) => path.includes('.')); return [...Object.keys(item), ...nestedColumns] .map((key) => getValueAtPath(item, key)) .filter((val) => typeof val !== 'object') .some((v) => { return searchRegex ? searchRegex.test(String(v)) : String(v).toLowerCase().includes(searchString); }); }; } 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=DataTableManager.js.map