flipper-plugin
Version:
Flipper Desktop plugin SDK and components
636 lines • 24.9 kB
JavaScript
/**
* 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
;