Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"context": "^3.0.4",
"global": "^4.4.0",
"lodash": "^4.17.21",
"minimatch": "^10.0.3",
"polished": "^4.2.2",
"prettier": "^2.7.1",
"react": "^18.2.0",
Expand Down
21 changes: 19 additions & 2 deletions src/devtoolApp/components/DownloadList/OptionSection/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useCallback } from 'react';
import { Toggle } from '../../Toggle';
import { OptionSectionWrapper } from './styles';
import { OptionSectionWrapper, FilterInputWrapper, FilterInput, FilterLabel } from './styles';
import * as optionActions from 'devtoolApp/store/option';
import useStore from 'devtoolApp/store';

export const OptionSection = () => {
const {
dispatch,
state: {
option: { ignoreNoContentFile, beautifyFile },
option: { ignoreNoContentFile, beautifyFile, urlFilter },
ui: { isSaving },
},
} = useStore();
Expand All @@ -21,8 +21,25 @@ export const OptionSection = () => {
dispatch(optionActions.setBeautifyFile(willBeautify));
}, []);

const handleUrlFilterChange = useCallback((e) => {
dispatch(optionActions.setUrlFilter(e.target.value));
}, []);

return (
<OptionSectionWrapper>
<FilterInputWrapper>
<FilterLabel htmlFor="url-filter">
URL Filter (supports glob patterns with negation, separate multiple rules with |):
</FilterLabel>
<FilterInput
id="url-filter"
type="text"
value={urlFilter}
onChange={handleUrlFilterChange}
placeholder="e.g.: *.js|*.css|!*test*|!*min.js, https://example.com/**|!https://example.com/temp/**"
disabled={isSaving}
/>
</FilterInputWrapper>
<Toggle noInteraction={isSaving} isToggled={ignoreNoContentFile} onToggle={handleIgnoreNoContentFile}>
Ignore "No Content" files
</Toggle>
Expand Down
32 changes: 32 additions & 0 deletions src/devtoolApp/components/DownloadList/OptionSection/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,35 @@ import styled from 'styled-components';
export const OptionSectionWrapper = styled.div`
padding: 0 20px 20px 20px;
`;

export const FilterInputWrapper = styled.div`
margin-bottom: 10px;
`;

export const FilterInput = styled.input`
width: 100%;
padding: 8px 12px;
border: 1px solid ${({ theme }) => theme.grayScale.gray5};
border-radius: 4px;
background-color: ${({ theme }) => theme.background};
color: ${({ theme }) => theme.text};
font-size: 14px;

&:focus {
outline: none;
border-color: ${({ theme }) => theme.primary};
}

&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;

export const FilterLabel = styled.label`
display: block;
margin-bottom: 4px;
font-size: 12px;
color: ${({ theme }) => theme.grayScale.gray10};
font-weight: 500;
`;
15 changes: 10 additions & 5 deletions src/devtoolApp/hooks/useAppSaveAllResource.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
import * as uiActions from '../store/ui';
import { downloadZipFile, resolveDuplicatedResources } from '../utils/file';
import { logResourceByUrl } from '../utils/resource';
import { globMatch } from '../utils/general';
import { resetNetworkResource } from '../store/networkResource';
import { resetStaticResource } from '../store/staticResource';
import { INITIAL_STATE as UI_INITIAL_STATE } from '../store/ui';
Expand All @@ -14,7 +15,7 @@ export const useAppSaveAllResource = () => {
const staticResourceRef = useRef(staticResource);
const {
downloadList,
option: { ignoreNoContentFile, beautifyFile },
option: { ignoreNoContentFile, beautifyFile, urlFilter },
ui: { tab },
} = state;

Expand Down Expand Up @@ -54,16 +55,20 @@ export const useAppSaveAllResource = () => {
...(networkResourceRef.current || []),
...(staticResourceRef.current || []),
]);
console.log(toDownload.filter(t => typeof t?.content !== 'string' && !!t?.content?.then));
if (loaded && toDownload.length) {

// Apply URL filter if specified
const filteredToDownload = urlFilter ? toDownload.filter((resource) => globMatch(urlFilter, resource.url)) : toDownload;

console.log(toDownload.filter((t) => typeof t?.content !== 'string' && !!t?.content?.then));
if (loaded && filteredToDownload.length) {
downloadZipFile(
toDownload,
filteredToDownload,
{ ignoreNoContentFile, beautifyFile },
(item, isDone) => {
dispatch(uiActions.setStatus(`Compressed: ${item.url} Processed: ${isDone}`));
},
() => {
logResourceByUrl(dispatch, downloadItem.url, toDownload);
logResourceByUrl(dispatch, downloadItem.url, filteredToDownload);
if (i + 1 !== downloadList.length) {
dispatch(resetNetworkResource());
dispatch(resetStaticResource());
Expand Down
27 changes: 27 additions & 0 deletions src/devtoolApp/store/option/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { getReducerConfig } from '../utils';
import { loadUrlFilterFromStorage, saveUrlFilterToStorage } from 'devtoolApp/utils/general';

export const STATE_KEY = `option`;

export const ACTIONS = {
SET_IGNORE_NO_CONTENT_FILE: 'SET_IGNORE_NO_CONTENT_FILE',
SET_BEAUTIFY_FILE: 'SET_BEAUTIFY_FILE',
SET_URL_FILTER: 'SET_URL_FILTER',
LOAD_URL_FILTER_FROM_STORAGE: 'LOAD_URL_FILTER_FROM_STORAGE',
};

export const INITIAL_STATE = {
ignoreNoContentFile: false,
beautifyFile: false,
urlFilter: loadUrlFilterFromStorage(), // Load from sessionStorage on initialization
};

export const setIgnoreNoContentFile = (willIgnore) => ({
Expand All @@ -22,6 +26,22 @@ export const setBeautifyFile = (willBeautify) => ({
payload: !!willBeautify,
});

export const setUrlFilter = (filter) => {
const urlFilter = filter || '';
// Save to sessionStorage when setting URL filter
saveUrlFilterToStorage(urlFilter);

return {
type: ACTIONS.SET_URL_FILTER,
payload: urlFilter,
};
};

export const loadUrlFilterFromStorageAction = () => ({
type: ACTIONS.LOAD_URL_FILTER_FROM_STORAGE,
payload: loadUrlFilterFromStorage(),
});

export const uiReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ACTIONS.SET_IGNORE_NO_CONTENT_FILE: {
Expand All @@ -36,6 +56,13 @@ export const uiReducer = (state = INITIAL_STATE, action) => {
beautifyFile: action.payload,
};
}
case ACTIONS.SET_URL_FILTER:
case ACTIONS.LOAD_URL_FILTER_FROM_STORAGE: {
return {
...state,
urlFilter: action.payload,
};
}
default: {
return state;
}
Expand Down
2 changes: 1 addition & 1 deletion src/devtoolApp/utils/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const getContentRead = (item) => {
export const addItemsToZipWriter = (zipWriter, items, options, eachDoneCallback, callback) => {
const item = items[0];
const rest = items.slice(1);

// console.log(item);
// if item exist so add it to zip
if (item) {
// Beautify here
Expand Down
81 changes: 81 additions & 0 deletions src/devtoolApp/utils/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,84 @@ export const logIfDev = (...props) => {
console.log('[DEVTOOL]', ...props);
}
};

import { minimatch } from 'minimatch';

/**
* Glob pattern matching using minimatch library
* Supports * (wildcard), ** (recursive wildcard), and negation with !
* Supports multiple patterns separated by | (pipe)
* @param {string} pattern - glob pattern(s), can be separated by |
* @param {string} str - string to match
* @returns {boolean} - whether the string matches the pattern
*/
export const globMatch = (pattern, str) => {
if (!pattern || pattern.trim() === '') return true; // Empty pattern matches everything
if (!str) return false;

// Split pattern by | to support multiple patterns
const patterns = pattern.split('|').map(p => p.trim()).filter(p => p);

// If no valid patterns after filtering, match everything
if (patterns.length === 0) return true;

// Separate positive and negative patterns
const positivePatterns = [];
const negativePatterns = [];

patterns.forEach(singlePattern => {
if (singlePattern.startsWith('!')) {
negativePatterns.push(singlePattern.slice(1)); // Remove the ! prefix
} else {
positivePatterns.push(singlePattern);
}
});

// If there are no positive patterns, default to match all
const hasPositiveMatch = positivePatterns.length === 0 || positivePatterns.some(pattern => {
try {
return minimatch(str, pattern, { nocase: true });
} catch (e) {
console.warn('[DEVTOOL] Invalid glob pattern:', pattern, e);
return false;
}
});

// If positive patterns don't match, return false
if (!hasPositiveMatch) return false;

// Check negative patterns - if any negative pattern matches, exclude this item
const hasNegativeMatch = negativePatterns.some(pattern => {
try {
return minimatch(str, pattern, { nocase: true });
} catch (e) {
console.warn('[DEVTOOL] Invalid negative glob pattern:', pattern, e);
return false;
}
});

// Return true if positive matches and no negative matches
return !hasNegativeMatch;
};

/**
* Storage utilities for URL filter persistence
*/
const URL_FILTER_STORAGE_KEY = 'resources_saver_url_filter';

export const saveUrlFilterToStorage = (filter) => {
try {
sessionStorage.setItem(URL_FILTER_STORAGE_KEY, filter || '');
} catch (e) {
console.warn('[DEVTOOL] Failed to save URL filter to sessionStorage:', e);
}
};

export const loadUrlFilterFromStorage = () => {
try {
return sessionStorage.getItem(URL_FILTER_STORAGE_KEY) || '';
} catch (e) {
console.warn('[DEVTOOL] Failed to load URL filter from sessionStorage:', e);
return '';
}
};