++ if (!this.preserveMultipleSlashes) {
++ for (let i = 1; i < parts.length - 1; i++) {
++ const p = parts[i];
++ // don't squeeze out UNC patterns
++ if (i === 1 && p === '' && parts[0] === '')
++ continue;
++ if (p === '.' || p === '') {
++ didSomething = true;
++ parts.splice(i, 1);
++ i--;
++ }
++ }
++ if (parts[0] === '.' &&
++ parts.length === 2 &&
++ (parts[1] === '.' || parts[1] === '')) {
++ didSomething = true;
++ parts.pop();
++ }
++ }
++ // //../ -> /
++ let dd = 0;
++ while (-1 !== (dd = parts.indexOf('..', dd + 1))) {
++ const p = parts[dd - 1];
++ if (p && p !== '.' && p !== '..' && p !== '**') {
++ didSomething = true;
++ parts.splice(dd - 1, 2);
++ dd -= 2;
++ }
++ }
++ } while (didSomething);
++ return parts.length === 0 ? [''] : parts;
++ }
++ // First phase: single-pattern processing
++ // is 1 or more portions
++ // is 1 or more portions
++ // is any portion other than ., .., '', or **
++ // is . or ''
++ //
++ // **/.. is *brutal* for filesystem walking performance, because
++ // it effectively resets the recursive walk each time it occurs,
++ // and ** cannot be reduced out by a .. pattern part like a regexp
++ // or most strings (other than .., ., and '') can be.
++ //
++ // /**/..//
/ -> {/..//
/,/**//
/}
++ // // -> /
++ // //../ -> /
++ // **/**/ -> **/
++ //
++ // **/*/ -> */**/ <== not valid because ** doesn't follow
++ // this WOULD be allowed if ** did follow symlinks, or * didn't
++ firstPhasePreProcess(globParts) {
++ let didSomething = false;
++ do {
++ didSomething = false;
++ // /**/..//
/ -> {/..//
/,/**//
/}
++ for (let parts of globParts) {
++ let gs = -1;
++ while (-1 !== (gs = parts.indexOf('**', gs + 1))) {
++ let gss = gs;
++ while (parts[gss + 1] === '**') {
++ // /**/**/ -> /**/
++ gss++;
++ }
++ // eg, if gs is 2 and gss is 4, that means we have 3 **
++ // parts, and can remove 2 of them.
++ if (gss > gs) {
++ parts.splice(gs + 1, gss - gs);
++ }
++ let next = parts[gs + 1];
++ const p = parts[gs + 2];
++ const p2 = parts[gs + 3];
++ if (next !== '..')
++ continue;
++ if (!p ||
++ p === '.' ||
++ p === '..' ||
++ !p2 ||
++ p2 === '.' ||
++ p2 === '..') {
++ continue;
++ }
++ didSomething = true;
++ // edit parts in place, and push the new one
++ parts.splice(gs, 1);
++ const other = parts.slice(0);
++ other[gs] = '**';
++ globParts.push(other);
++ gs--;
++ }
++ // // -> /
++ if (!this.preserveMultipleSlashes) {
++ for (let i = 1; i < parts.length - 1; i++) {
++ const p = parts[i];
++ // don't squeeze out UNC patterns
++ if (i === 1 && p === '' && parts[0] === '')
++ continue;
++ if (p === '.' || p === '') {
++ didSomething = true;
++ parts.splice(i, 1);
++ i--;
++ }
++ }
++ if (parts[0] === '.' &&
++ parts.length === 2 &&
++ (parts[1] === '.' || parts[1] === '')) {
++ didSomething = true;
++ parts.pop();
++ }
++ }
++ // //../ -> /
++ let dd = 0;
++ while (-1 !== (dd = parts.indexOf('..', dd + 1))) {
++ const p = parts[dd - 1];
++ if (p && p !== '.' && p !== '..' && p !== '**') {
++ didSomething = true;
++ const needDot = dd === 1 && parts[dd + 1] === '**';
++ const splin = needDot ? ['.'] : [];
++ parts.splice(dd - 1, 2, ...splin);
++ if (parts.length === 0)
++ parts.push('');
++ dd -= 2;
++ }
++ }
++ }
++ } while (didSomething);
++ return globParts;
++ }
++ // second phase: multi-pattern dedupes
++ // {/*/,//} -> /*/
++ // {/,/} -> /
++ // {/**/,/} -> /**/
++ //
++ // {/**/,/**//} -> /**/
++ // ^-- not valid because ** doens't follow symlinks
++ secondPhasePreProcess(globParts) {
++ for (let i = 0; i < globParts.length - 1; i++) {
++ for (let j = i + 1; j < globParts.length; j++) {
++ const matched = this.partsMatch(globParts[i], globParts[j], !this.preserveMultipleSlashes);
++ if (matched) {
++ globParts[i] = [];
++ globParts[j] = matched;
++ break;
++ }
++ }
++ }
++ return globParts.filter(gs => gs.length);
++ }
++ partsMatch(a, b, emptyGSMatch = false) {
++ let ai = 0;
++ let bi = 0;
++ let result = [];
++ let which = '';
++ while (ai < a.length && bi < b.length) {
++ if (a[ai] === b[bi]) {
++ result.push(which === 'b' ? b[bi] : a[ai]);
++ ai++;
++ bi++;
++ }
++ else if (emptyGSMatch && a[ai] === '**' && b[bi] === a[ai + 1]) {
++ result.push(a[ai]);
++ ai++;
++ }
++ else if (emptyGSMatch && b[bi] === '**' && a[ai] === b[bi + 1]) {
++ result.push(b[bi]);
++ bi++;
++ }
++ else if (a[ai] === '*' &&
++ b[bi] &&
++ (this.options.dot || !b[bi].startsWith('.')) &&
++ b[bi] !== '**') {
++ if (which === 'b')
++ return false;
++ which = 'a';
++ result.push(a[ai]);
++ ai++;
++ bi++;
++ }
++ else if (b[bi] === '*' &&
++ a[ai] &&
++ (this.options.dot || !a[ai].startsWith('.')) &&
++ a[ai] !== '**') {
++ if (which === 'a')
++ return false;
++ which = 'b';
++ result.push(b[bi]);
++ ai++;
++ bi++;
++ }
++ else {
++ return false;
++ }
++ }
++ // if we fall out of the loop, it means they two are identical
++ // as long as their lengths match
++ return a.length === b.length && result;
++ }
++ parseNegate() {
++ if (this.nonegate)
++ return;
++ const pattern = this.pattern;
++ let negate = false;
++ let negateOffset = 0;
++ for (let i = 0; i < pattern.length && pattern.charAt(i) === '!'; i++) {
++ negate = !negate;
++ negateOffset++;
++ }
++ if (negateOffset)
++ this.pattern = pattern.slice(negateOffset);
++ this.negate = negate;
++ }
++ // set partial to true to test if, for example,
++ // "/a/b" matches the start of "/*/b/*/d"
++ // Partial means, if you run out of file before you run
++ // out of pattern, then that's fine, as long as all
++ // the parts match.
++ matchOne(file, pattern, partial = false) {
++ const options = this.options;
++ // UNC paths like //?/X:/... can match X:/... and vice versa
++ // Drive letters in absolute drive or unc paths are always compared
++ // case-insensitively.
++ if (this.isWindows) {
++ const fileDrive = typeof file[0] === 'string' && /^[a-z]:$/i.test(file[0]);
++ const fileUNC = !fileDrive &&
++ file[0] === '' &&
++ file[1] === '' &&
++ file[2] === '?' &&
++ /^[a-z]:$/i.test(file[3]);
++ const patternDrive = typeof pattern[0] === 'string' && /^[a-z]:$/i.test(pattern[0]);
++ const patternUNC = !patternDrive &&
++ pattern[0] === '' &&
++ pattern[1] === '' &&
++ pattern[2] === '?' &&
++ typeof pattern[3] === 'string' &&
++ /^[a-z]:$/i.test(pattern[3]);
++ const fdi = fileUNC ? 3 : fileDrive ? 0 : undefined;
++ const pdi = patternUNC ? 3 : patternDrive ? 0 : undefined;
++ if (typeof fdi === 'number' && typeof pdi === 'number') {
++ const [fd, pd] = [file[fdi], pattern[pdi]];
++ if (fd.toLowerCase() === pd.toLowerCase()) {
++ pattern[pdi] = fd;
++ if (pdi > fdi) {
++ pattern = pattern.slice(pdi);
++ }
++ else if (fdi > pdi) {
++ file = file.slice(fdi);
++ }
++ }
++ }
++ }
++ // resolve and reduce . and .. portions in the file as well.
++ // dont' need to do the second phase, because it's only one string[]
++ const { optimizationLevel = 1 } = this.options;
++ if (optimizationLevel >= 2) {
++ file = this.levelTwoFileOptimize(file);
++ }
++ this.debug('matchOne', this, { file, pattern });
++ this.debug('matchOne', file.length, pattern.length);
++ for (var fi = 0, pi = 0, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) {
++ this.debug('matchOne loop');
++ var p = pattern[pi];
++ var f = file[fi];
++ this.debug(pattern, p, f);
++ // should be impossible.
++ // some invalid regexp stuff in the set.
++ /* c8 ignore start */
++ if (p === false) {
++ return false;
++ }
++ /* c8 ignore stop */
++ if (p === exports.GLOBSTAR) {
++ this.debug('GLOBSTAR', [pattern, p, f]);
++ // "**"
++ // a/**/b/**/c would match the following:
++ // a/b/x/y/z/c
++ // a/x/y/z/b/c
++ // a/b/x/b/x/c
++ // a/b/c
++ // To do this, take the rest of the pattern after
++ // the **, and see if it would match the file remainder.
++ // If so, return success.
++ // If not, the ** "swallows" a segment, and try again.
++ // This is recursively awful.
++ //
++ // a/**/b/**/c matching a/b/x/y/z/c
++ // - a matches a
++ // - doublestar
++ // - matchOne(b/x/y/z/c, b/**/c)
++ // - b matches b
++ // - doublestar
++ // - matchOne(x/y/z/c, c) -> no
++ // - matchOne(y/z/c, c) -> no
++ // - matchOne(z/c, c) -> no
++ // - matchOne(c, c) yes, hit
++ var fr = fi;
++ var pr = pi + 1;
++ if (pr === pl) {
++ this.debug('** at the end');
++ // a ** at the end will just swallow the rest.
++ // We have found a match.
++ // however, it will not swallow /.x, unless
++ // options.dot is set.
++ // . and .. are *never* matched by **, for explosively
++ // exponential reasons.
++ for (; fi < fl; fi++) {
++ if (file[fi] === '.' ||
++ file[fi] === '..' ||
++ (!options.dot && file[fi].charAt(0) === '.'))
++ return false;
++ }
++ return true;
++ }
++ // ok, let's see if we can swallow whatever we can.
++ while (fr < fl) {
++ var swallowee = file[fr];
++ this.debug('\nglobstar while', file, fr, pattern, pr, swallowee);
++ // XXX remove this slice. Just pass the start index.
++ if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
++ this.debug('globstar found match!', fr, fl, swallowee);
++ // found a match.
++ return true;
++ }
++ else {
++ // can't swallow "." or ".." ever.
++ // can only swallow ".foo" when explicitly asked.
++ if (swallowee === '.' ||
++ swallowee === '..' ||
++ (!options.dot && swallowee.charAt(0) === '.')) {
++ this.debug('dot detected!', file, fr, pattern, pr);
++ break;
++ }
++ // ** swallows a segment, and continue.
++ this.debug('globstar swallow a segment, and continue');
++ fr++;
++ }
++ }
++ // no match was found.
++ // However, in partial mode, we can't say this is necessarily over.
++ /* c8 ignore start */
++ if (partial) {
++ // ran out of file
++ this.debug('\n>>> no match, partial?', file, fr, pattern, pr);
++ if (fr === fl) {
++ return true;
++ }
++ }
++ /* c8 ignore stop */
++ return false;
++ }
++ // something other than **
++ // non-magic patterns just have to match exactly
++ // patterns with magic have been turned into regexps.
++ let hit;
++ if (typeof p === 'string') {
++ hit = f === p;
++ this.debug('string match', p, f, hit);
++ }
++ else {
++ hit = p.test(f);
++ this.debug('pattern match', p, f, hit);
++ }
++ if (!hit)
++ return false;
++ }
++ // Note: ending in / means that we'll get a final ""
++ // at the end of the pattern. This can only match a
++ // corresponding "" at the end of the file.
++ // If the file ends in /, then it can only match a
++ // a pattern that ends in /, unless the pattern just
++ // doesn't have any more for it. But, a/b/ should *not*
++ // match "a/b/*", even though "" matches against the
++ // [^/]*? pattern, except in partial mode, where it might
++ // simply not be reached yet.
++ // However, a/b/ should still satisfy a/*
++ // now either we fell off the end of the pattern, or we're done.
++ if (fi === fl && pi === pl) {
++ // ran out of pattern and filename at the same time.
++ // an exact hit!
++ return true;
++ }
++ else if (fi === fl) {
++ // ran out of file, but still had pattern left.
++ // this is ok if we're doing the match as part of
++ // a glob fs traversal.
++ return partial;
++ }
++ else if (pi === pl) {
++ // ran out of pattern, still have file left.
++ // this is only acceptable if we're on the very last
++ // empty segment of a file with a trailing slash.
++ // a/* should match a/b/
++ return fi === fl - 1 && file[fi] === '';
++ /* c8 ignore start */
++ }
++ else {
++ // should be unreachable.
++ throw new Error('wtf?');
++ }
++ /* c8 ignore stop */
++ }
++ braceExpand() {
++ return (0, exports.braceExpand)(this.pattern, this.options);
++ }
++ parse(pattern) {
++ (0, assert_valid_pattern_js_1.assertValidPattern)(pattern);
++ const options = this.options;
++ // shortcuts
++ if (pattern === '**')
++ return exports.GLOBSTAR;
++ if (pattern === '')
++ return '';
++ // far and away, the most common glob pattern parts are
++ // *, *.*, and *. Add a fast check method for those.
++ let m;
++ let fastTest = null;
++ if ((m = pattern.match(starRE))) {
++ fastTest = options.dot ? starTestDot : starTest;
++ }
++ else if ((m = pattern.match(starDotExtRE))) {
++ fastTest = (options.nocase
++ ? options.dot
++ ? starDotExtTestNocaseDot
++ : starDotExtTestNocase
++ : options.dot
++ ? starDotExtTestDot
++ : starDotExtTest)(m[1]);
++ }
++ else if ((m = pattern.match(qmarksRE))) {
++ fastTest = (options.nocase
++ ? options.dot
++ ? qmarksTestNocaseDot
++ : qmarksTestNocase
++ : options.dot
++ ? qmarksTestDot
++ : qmarksTest)(m);
++ }
++ else if ((m = pattern.match(starDotStarRE))) {
++ fastTest = options.dot ? starDotStarTestDot : starDotStarTest;
++ }
++ else if ((m = pattern.match(dotStarRE))) {
++ fastTest = dotStarTest;
++ }
++ const re = ast_js_1.AST.fromGlob(pattern, this.options).toMMPattern();
++ if (fastTest && typeof re === 'object') {
++ // Avoids overriding in frozen environments
++ Reflect.defineProperty(re, 'test', { value: fastTest });
++ }
++ return re;
++ }
++ makeRe() {
++ if (this.regexp || this.regexp === false)
++ return this.regexp;
++ // at this point, this.set is a 2d array of partial
++ // pattern strings, or "**".
++ //
++ // It's better to use .match(). This function shouldn't
++ // be used, really, but it's pretty convenient sometimes,
++ // when you just want to work with a regex.
++ const set = this.set;
++ if (!set.length) {
++ this.regexp = false;
++ return this.regexp;
++ }
++ const options = this.options;
++ const twoStar = options.noglobstar
++ ? star
++ : options.dot
++ ? twoStarDot
++ : twoStarNoDot;
++ const flags = new Set(options.nocase ? ['i'] : []);
++ // regexpify non-globstar patterns
++ // if ** is only item, then we just do one twoStar
++ // if ** is first, and there are more, prepend (\/|twoStar\/)? to next
++ // if ** is last, append (\/twoStar|) to previous
++ // if ** is in the middle, append (\/|\/twoStar\/) to previous
++ // then filter out GLOBSTAR symbols
++ let re = set
++ .map(pattern => {
++ const pp = pattern.map(p => {
++ if (p instanceof RegExp) {
++ for (const f of p.flags.split(''))
++ flags.add(f);
++ }
++ return typeof p === 'string'
++ ? regExpEscape(p)
++ : p === exports.GLOBSTAR
++ ? exports.GLOBSTAR
++ : p._src;
++ });
++ pp.forEach((p, i) => {
++ const next = pp[i + 1];
++ const prev = pp[i - 1];
++ if (p !== exports.GLOBSTAR || prev === exports.GLOBSTAR) {
++ return;
++ }
++ if (prev === undefined) {
++ if (next !== undefined && next !== exports.GLOBSTAR) {
++ pp[i + 1] = '(?:\\/|' + twoStar + '\\/)?' + next;
++ }
++ else {
++ pp[i] = twoStar;
++ }
++ }
++ else if (next === undefined) {
++ pp[i - 1] = prev + '(?:\\/|' + twoStar + ')?';
++ }
++ else if (next !== exports.GLOBSTAR) {
++ pp[i - 1] = prev + '(?:\\/|\\/' + twoStar + '\\/)' + next;
++ pp[i + 1] = exports.GLOBSTAR;
++ }
++ });
++ return pp.filter(p => p !== exports.GLOBSTAR).join('/');
++ })
++ .join('|');
++ // need to wrap in parens if we had more than one thing with |,
++ // otherwise only the first will be anchored to ^ and the last to $
++ const [open, close] = set.length > 1 ? ['(?:', ')'] : ['', ''];
++ // must match entire pattern
++ // ending in a * or ** will make it less strict.
++ re = '^' + open + re + close + '$';
++ // can match anything, as long as it's not this.
++ if (this.negate)
++ re = '^(?!' + re + ').+$';
++ try {
++ this.regexp = new RegExp(re, [...flags].join(''));
++ /* c8 ignore start */
++ }
++ catch (ex) {
++ // should be impossible
++ this.regexp = false;
++ }
++ /* c8 ignore stop */
++ return this.regexp;
++ }
++ slashSplit(p) {
++ // if p starts with // on windows, we preserve that
++ // so that UNC paths aren't broken. Otherwise, any number of
++ // / characters are coalesced into one, unless
++ // preserveMultipleSlashes is set to true.
++ if (this.preserveMultipleSlashes) {
++ return p.split('/');
++ }
++ else if (this.isWindows && /^\/\/[^\/]+/.test(p)) {
++ // add an extra '' for the one we lose
++ return ['', ...p.split(/\/+/)];
++ }
++ else {
++ return p.split(/\/+/);
++ }
++ }
++ match(f, partial = this.partial) {
++ this.debug('match', f, this.pattern);
++ // short-circuit in the case of busted things.
++ // comments, etc.
++ if (this.comment) {
++ return false;
++ }
++ if (this.empty) {
++ return f === '';
++ }
++ if (f === '/' && partial) {
++ return true;
++ }
++ const options = this.options;
++ // windows: need to use /, not \
++ if (this.isWindows) {
++ f = f.split('\\').join('/');
++ }
++ // treat the test path as a set of pathparts.
++ const ff = this.slashSplit(f);
++ this.debug(this.pattern, 'split', ff);
++ // just ONE of the pattern sets in this.set needs to match
++ // in order for it to be valid. If negating, then just one
++ // match means that we have failed.
++ // Either way, return on the first hit.
++ const set = this.set;
++ this.debug(this.pattern, 'set', set);
++ // Find the basename of the path by looking for the last non-empty segment
++ let filename = ff[ff.length - 1];
++ if (!filename) {
++ for (let i = ff.length - 2; !filename && i >= 0; i--) {
++ filename = ff[i];
++ }
++ }
++ for (let i = 0; i < set.length; i++) {
++ const pattern = set[i];
++ let file = ff;
++ if (options.matchBase && pattern.length === 1) {
++ file = [filename];
++ }
++ const hit = this.matchOne(file, pattern, partial);
++ if (hit) {
++ if (options.flipNegate) {
++ return true;
++ }
++ return !this.negate;
++ }
++ }
++ // didn't get any hits. this is success if it's a negative
++ // pattern, failure otherwise.
++ if (options.flipNegate) {
++ return false;
++ }
++ return this.negate;
++ }
++ static defaults(def) {
++ return exports.minimatch.defaults(def).Minimatch;
++ }
++ }
++ exports.Minimatch = Minimatch;
++ /* c8 ignore start */
++ var ast_js_2 = ast;
++ Object.defineProperty(exports, "AST", { enumerable: true, get: function () { return ast_js_2.AST; } });
++ var escape_js_2 = _escape;
++ Object.defineProperty(exports, "escape", { enumerable: true, get: function () { return escape_js_2.escape; } });
++ var unescape_js_2 = _unescape;
++ Object.defineProperty(exports, "unescape", { enumerable: true, get: function () { return unescape_js_2.unescape; } });
++ /* c8 ignore stop */
++ exports.minimatch.AST = ast_js_1.AST;
++ exports.minimatch.Minimatch = Minimatch;
++ exports.minimatch.escape = escape_js_1.escape;
++ exports.minimatch.unescape = unescape_js_1.unescape;
++
++} (commonjs));
++
++const fs = require$$0;
++const path = path$1;
++const EE = require$$2.EventEmitter;
++const Minimatch = commonjs.Minimatch;
++
++class Walker extends EE {
++ constructor (opts) {
++ opts = opts || {};
++ super(opts);
++ // set to true if this.path is a symlink, whether follow is true or not
++ this.isSymbolicLink = opts.isSymbolicLink;
++ this.path = opts.path || process.cwd();
++ this.basename = path.basename(this.path);
++ this.ignoreFiles = opts.ignoreFiles || ['.ignore'];
++ this.ignoreRules = {};
++ this.parent = opts.parent || null;
++ this.includeEmpty = !!opts.includeEmpty;
++ this.root = this.parent ? this.parent.root : this.path;
++ this.follow = !!opts.follow;
++ this.result = this.parent ? this.parent.result : new Set();
++ this.entries = null;
++ this.sawError = false;
++ this.exact = opts.exact;
++ }
++
++ sort (a, b) {
++ return a.localeCompare(b, 'en')
++ }
++
++ emit (ev, data) {
++ let ret = false;
++ if (!(this.sawError && ev === 'error')) {
++ if (ev === 'error') {
++ this.sawError = true;
++ } else if (ev === 'done' && !this.parent) {
++ data = Array.from(data)
++ .map(e => /^@/.test(e) ? `./${e}` : e).sort(this.sort);
++ this.result = data;
++ }
++
++ if (ev === 'error' && this.parent) {
++ ret = this.parent.emit('error', data);
++ } else {
++ ret = super.emit(ev, data);
++ }
++ }
++ return ret
++ }
++
++ start () {
++ fs.readdir(this.path, (er, entries) =>
++ er ? this.emit('error', er) : this.onReaddir(entries));
++ return this
++ }
++
++ isIgnoreFile (e) {
++ return e !== '.' &&
++ e !== '..' &&
++ this.ignoreFiles.indexOf(e) !== -1
++ }
++
++ onReaddir (entries) {
++ this.entries = entries;
++ if (entries.length === 0) {
++ if (this.includeEmpty) {
++ this.result.add(this.path.slice(this.root.length + 1));
++ }
++ this.emit('done', this.result);
++ } else {
++ const hasIg = this.entries.some(e =>
++ this.isIgnoreFile(e));
++
++ if (hasIg) {
++ this.addIgnoreFiles();
++ } else {
++ this.filterEntries();
++ }
++ }
++ }
++
++ addIgnoreFiles () {
++ const newIg = this.entries
++ .filter(e => this.isIgnoreFile(e));
++
++ let igCount = newIg.length;
++ const then = () => {
++ if (--igCount === 0) {
++ this.filterEntries();
++ }
++ };
++
++ newIg.forEach(e => this.addIgnoreFile(e, then));
++ }
++
++ addIgnoreFile (file, then) {
++ const ig = path.resolve(this.path, file);
++ fs.readFile(ig, 'utf8', (er, data) =>
++ er ? this.emit('error', er) : this.onReadIgnoreFile(file, data, then));
++ }
++
++ onReadIgnoreFile (file, data, then) {
++ const mmopt = {
++ matchBase: true,
++ dot: true,
++ flipNegate: true,
++ nocase: true,
++ };
++ const rules = data.split(/\r?\n/)
++ .filter(line => !/^#|^$/.test(line.trim()))
++ .map(rule => {
++ return new Minimatch(rule.trim(), mmopt)
++ });
++
++ this.ignoreRules[file] = rules;
++
++ then();
++ }
++
++ filterEntries () {
++ // at this point we either have ignore rules, or just inheriting
++ // this exclusion is at the point where we know the list of
++ // entries in the dir, but don't know what they are. since
++ // some of them *might* be directories, we have to run the
++ // match in dir-mode as well, so that we'll pick up partials
++ // of files that will be included later. Anything included
++ // at this point will be checked again later once we know
++ // what it is.
++ const filtered = this.entries.map(entry => {
++ // at this point, we don't know if it's a dir or not.
++ const passFile = this.filterEntry(entry);
++ const passDir = this.filterEntry(entry, true);
++ return (passFile || passDir) ? [entry, passFile, passDir] : false
++ }).filter(e => e);
++
++ // now we stat them all
++ // if it's a dir, and passes as a dir, then recurse
++ // if it's not a dir, but passes as a file, add to set
++ let entryCount = filtered.length;
++ if (entryCount === 0) {
++ this.emit('done', this.result);
++ } else {
++ const then = () => {
++ if (--entryCount === 0) {
++ this.emit('done', this.result);
++ }
++ };
++ filtered.forEach(filt => {
++ const entry = filt[0];
++ const file = filt[1];
++ const dir = filt[2];
++ this.stat({ entry, file, dir }, then);
++ });
++ }
++ }
++
++ onstat ({ st, entry, file, dir, isSymbolicLink }, then) {
++ const abs = this.path + '/' + entry;
++ if (!st.isDirectory()) {
++ if (file) {
++ this.result.add(abs.slice(this.root.length + 1));
++ }
++ then();
++ } else {
++ // is a directory
++ if (dir) {
++ this.walker(entry, { isSymbolicLink, exact: file || this.filterEntry(entry + '/') }, then);
++ } else {
++ then();
++ }
++ }
++ }
++
++ stat ({ entry, file, dir }, then) {
++ const abs = this.path + '/' + entry;
++ fs.lstat(abs, (lstatErr, lstatResult) => {
++ if (lstatErr) {
++ this.emit('error', lstatErr);
++ } else {
++ const isSymbolicLink = lstatResult.isSymbolicLink();
++ if (this.follow && isSymbolicLink) {
++ fs.stat(abs, (statErr, statResult) => {
++ if (statErr) {
++ this.emit('error', statErr);
++ } else {
++ this.onstat({ st: statResult, entry, file, dir, isSymbolicLink }, then);
++ }
++ });
++ } else {
++ this.onstat({ st: lstatResult, entry, file, dir, isSymbolicLink }, then);
++ }
++ }
++ });
++ }
++
++ walkerOpt (entry, opts) {
++ return {
++ path: this.path + '/' + entry,
++ parent: this,
++ ignoreFiles: this.ignoreFiles,
++ follow: this.follow,
++ includeEmpty: this.includeEmpty,
++ ...opts,
++ }
++ }
++
++ walker (entry, opts, then) {
++ new Walker(this.walkerOpt(entry, opts)).on('done', then).start();
++ }
++
++ filterEntry (entry, partial, entryBasename) {
++ let included = true;
++
++ // this = /a/b/c
++ // entry = d
++ // parent /a/b sees c/d
++ if (this.parent && this.parent.filterEntry) {
++ const parentEntry = this.basename + '/' + entry;
++ const parentBasename = entryBasename || entry;
++ included = this.parent.filterEntry(parentEntry, partial, parentBasename);
++ if (!included && !this.exact) {
++ return false
++ }
++ }
++
++ this.ignoreFiles.forEach(f => {
++ if (this.ignoreRules[f]) {
++ this.ignoreRules[f].forEach(rule => {
++ // negation means inclusion
++ // so if it's negated, and already included, no need to check
++ // likewise if it's neither negated nor included
++ if (rule.negate !== included) {
++ const isRelativeRule = entryBasename && rule.globParts.some(part =>
++ part.length <= (part.slice(-1)[0] ? 1 : 2)
++ );
++
++ // first, match against /foo/bar
++ // then, against foo/bar
++ // then, in the case of partials, match with a /
++ // then, if also the rule is relative, match against basename
++ const match = rule.match('/' + entry) ||
++ rule.match(entry) ||
++ !!partial && (
++ rule.match('/' + entry + '/') ||
++ rule.match(entry + '/') ||
++ rule.negate && (
++ rule.match('/' + entry, true) ||
++ rule.match(entry, true)) ||
++ isRelativeRule && (
++ rule.match('/' + entryBasename + '/') ||
++ rule.match(entryBasename + '/') ||
++ rule.negate && (
++ rule.match('/' + entryBasename, true) ||
++ rule.match(entryBasename, true))));
++
++ if (match) {
++ included = rule.negate;
++ }
++ }
++ });
++ }
++ });
++
++ return included
++ }
++}
++
++class WalkerSync extends Walker {
++ start () {
++ this.onReaddir(fs.readdirSync(this.path));
++ return this
++ }
++
++ addIgnoreFile (file, then) {
++ const ig = path.resolve(this.path, file);
++ this.onReadIgnoreFile(file, fs.readFileSync(ig, 'utf8'), then);
++ }
++
++ stat ({ entry, file, dir }, then) {
++ const abs = this.path + '/' + entry;
++ let st = fs.lstatSync(abs);
++ const isSymbolicLink = st.isSymbolicLink();
++ if (this.follow && isSymbolicLink) {
++ st = fs.statSync(abs);
++ }
++
++ // console.error('STAT SYNC', {st, entry, file, dir, isSymbolicLink, then})
++ this.onstat({ st, entry, file, dir, isSymbolicLink }, then);
++ }
++
++ walker (entry, opts, then) {
++ new WalkerSync(this.walkerOpt(entry, opts)).start();
++ then();
++ }
++}
++
++const walk$1 = (opts, callback) => {
++ const p = new Promise((resolve, reject) => {
++ new Walker(opts).on('done', resolve).on('error', reject).start();
++ });
++ return callback ? p.then(res => callback(null, res), callback) : p
++};
++
++const walkSync = opts => new WalkerSync(opts).start().result;
++
++var lib$1 = walk$1;
++walk$1.sync = walkSync;
++walk$1.Walker = Walker;
++walk$1.WalkerSync = WalkerSync;
++
++const { Walker: IgnoreWalker } = lib$1;
++const { lstatSync: lstat, readFileSync: readFile } = require$$0;
++const { basename, dirname, extname, join, relative, resolve, sep } = path$1;
++
++// symbols used to represent synthetic rule sets
++const defaultRules = Symbol('npm-packlist.rules.default');
++const strictRules = Symbol('npm-packlist.rules.strict');
++
++// There may be others, but :?|<> are handled by node-tar
++const nameIsBadForWindows = file => /\*/.test(file);
++
++// these are the default rules that are applied to everything except for non-link bundled deps
++const defaults = [
++ '.npmignore',
++ '.gitignore',
++ '**/.git',
++ '**/.svn',
++ '**/.hg',
++ '**/CVS',
++ '**/.git/**',
++ '**/.svn/**',
++ '**/.hg/**',
++ '**/CVS/**',
++ '/.lock-wscript',
++ '/.wafpickle-*',
++ '/build/config.gypi',
++ 'npm-debug.log',
++ '**/.npmrc',
++ '.*.swp',
++ '.DS_Store',
++ '**/.DS_Store/**',
++ '._*',
++ '**/._*/**',
++ '*.orig',
++ '/archived-packages/**',
++];
++
++const strictDefaults = [
++ // these are forcibly excluded
++ '/.git',
++];
++
++const normalizePath = (path) => path.split('\\').join('/');
++
++const readOutOfTreeIgnoreFiles = (root, rel, result = []) => {
++ for (const file of ['.npmignore', '.gitignore']) {
++ try {
++ const ignoreContent = readFile(join(root, file), { encoding: 'utf8' });
++ result.push(ignoreContent);
++ // break the loop immediately after reading, this allows us to prioritize
++ // the .npmignore and discard the .gitignore if one is present
++ break
++ } catch (err) {
++ // we ignore ENOENT errors completely because we don't care if the file doesn't exist
++ // but we throw everything else because failing to read a file that does exist is
++ // something that the user likely wants to know about
++ // istanbul ignore next -- we do not need to test a thrown error
++ if (err.code !== 'ENOENT') {
++ throw err
++ }
++ }
++ }
++
++ if (!rel) {
++ return result
++ }
++
++ const firstRel = rel.split(sep, 1)[0];
++ const newRoot = join(root, firstRel);
++ const newRel = relative(newRoot, join(root, rel));
++
++ return readOutOfTreeIgnoreFiles(newRoot, newRel, result)
++};
++
++class PackWalker extends IgnoreWalker {
++ constructor (tree, opts) {
++ const options = {
++ ...opts,
++ includeEmpty: false,
++ follow: false,
++ // we path.resolve() here because ignore-walk doesn't do it and we want full paths
++ path: resolve(opts?.path || tree.path).replace(/\\/g, '/'),
++ ignoreFiles: opts?.ignoreFiles || [
++ defaultRules,
++ 'package.json',
++ '.npmignore',
++ '.gitignore',
++ strictRules,
++ ],
++ };
++
++ super(options);
++ this.isPackage = options.isPackage;
++ this.seen = options.seen || new Set();
++ this.tree = tree;
++ this.requiredFiles = options.requiredFiles || [];
++
++ const additionalDefaults = [];
++ if (options.prefix && options.workspaces) {
++ const path = normalizePath(options.path);
++ const prefix = normalizePath(options.prefix);
++ const workspaces = options.workspaces.map((ws) => normalizePath(ws));
++
++ // istanbul ignore else - this does nothing unless we need it to
++ if (path !== prefix && workspaces.includes(path)) {
++ // if path and prefix are not the same directory, and workspaces has path in it
++ // then we know path is a workspace directory. in order to not drop ignore rules
++ // from directories between the workspaces root (prefix) and the workspace itself
++ // (path) we need to find and read those now
++ const relpath = relative(options.prefix, dirname(options.path));
++ additionalDefaults.push(...readOutOfTreeIgnoreFiles(options.prefix, relpath));
++ } else if (path === prefix) {
++ // on the other hand, if the path and prefix are the same, then we ignore workspaces
++ // so that we don't pack a workspace as part of the root project. append them as
++ // normalized relative paths from the root
++ additionalDefaults.push(...workspaces.map((w) => normalizePath(relative(options.path, w))));
++ }
++ }
++
++ // go ahead and inject the default rules now
++ this.injectRules(defaultRules, [...defaults, ...additionalDefaults]);
++
++ if (!this.isPackage) {
++ // if this instance is not a package, then place some strict default rules, and append
++ // known required files for this directory
++ this.injectRules(strictRules, [
++ ...strictDefaults,
++ ...this.requiredFiles.map((file) => `!${file}`),
++ ]);
++ }
++ }
++
++ // overridden method: we intercept the reading of the package.json file here so that we can
++ // process it into both the package.json file rules as well as the strictRules synthetic rule set
++ addIgnoreFile (file, callback) {
++ // if we're adding anything other than package.json, then let ignore-walk handle it
++ if (file !== 'package.json' || !this.isPackage) {
++ return super.addIgnoreFile(file, callback)
++ }
++
++ return this.processPackage(callback)
++ }
++
++ // overridden method: if we're done, but we're a package, then we also need to evaluate bundles
++ // before we actually emit our done event
++ emit (ev, data) {
++ if (ev !== 'done' || !this.isPackage) {
++ return super.emit(ev, data)
++ }
++
++ // we intentionally delay the done event while keeping the function sync here
++ // eslint-disable-next-line promise/catch-or-return, promise/always-return
++ this.gatherBundles().then(() => {
++ super.emit('done', this.result);
++ });
++ return true
++ }
++
++ // overridden method: before actually filtering, we make sure that we've removed the rules for
++ // files that should no longer take effect due to our order of precedence
++ filterEntries () {
++ if (this.ignoreRules['package.json']) {
++ // package.json means no .npmignore or .gitignore
++ this.ignoreRules['.npmignore'] = null;
++ this.ignoreRules['.gitignore'] = null;
++ } else if (this.ignoreRules['.npmignore']) {
++ // .npmignore means no .gitignore
++ this.ignoreRules['.gitignore'] = null;
++ }
++
++ return super.filterEntries()
++ }
++
++ // overridden method: we never want to include anything that isn't a file or directory
++ onstat (opts, callback) {
++ if (!opts.st.isFile() && !opts.st.isDirectory()) {
++ return callback()
++ }
++
++ return super.onstat(opts, callback)
++ }
++
++ // overridden method: we want to refuse to pack files that are invalid, node-tar protects us from
++ // a lot of them but not all
++ stat (opts, callback) {
++ if (nameIsBadForWindows(opts.entry)) {
++ return callback()
++ }
++
++ return super.stat(opts, callback)
++ }
++
++ // overridden method: this is called to create options for a child walker when we step
++ // in to a normal child directory (this will never be a bundle). the default method here
++ // copies the root's `ignoreFiles` value, but we don't want to respect package.json for
++ // subdirectories, so we override it with a list that intentionally omits package.json
++ walkerOpt (entry, opts) {
++ let ignoreFiles = null;
++
++ // however, if we have a tree, and we have workspaces, and the directory we're about
++ // to step into is a workspace, then we _do_ want to respect its package.json
++ if (this.tree.workspaces) {
++ const workspaceDirs = [...this.tree.workspaces.values()]
++ .map((dir) => dir.replace(/\\/g, '/'));
++
++ const entryPath = join(this.path, entry).replace(/\\/g, '/');
++ if (workspaceDirs.includes(entryPath)) {
++ ignoreFiles = [
++ defaultRules,
++ 'package.json',
++ '.npmignore',
++ '.gitignore',
++ strictRules,
++ ];
++ }
++ } else {
++ ignoreFiles = [
++ defaultRules,
++ '.npmignore',
++ '.gitignore',
++ strictRules,
++ ];
++ }
++
++ return {
++ ...super.walkerOpt(entry, opts),
++ ignoreFiles,
++ // we map over our own requiredFiles and pass ones that are within this entry
++ requiredFiles: this.requiredFiles
++ .map((file) => {
++ if (relative(file, entry) === '..') {
++ return relative(entry, file).replace(/\\/g, '/')
++ }
++ return false
++ })
++ .filter(Boolean),
++ }
++ }
++
++ // overridden method: we want child walkers to be instances of this class, not ignore-walk
++ walker (entry, opts, callback) {
++ new PackWalker(this.tree, this.walkerOpt(entry, opts)).on('done', callback).start();
++ }
++
++ // overridden method: we use a custom sort method to help compressibility
++ sort (a, b) {
++ // optimize for compressibility
++ // extname, then basename, then locale alphabetically
++ // https://twitter.com/isntitvacant/status/1131094910923231232
++ const exta = extname(a).toLowerCase();
++ const extb = extname(b).toLowerCase();
++ const basea = basename(a).toLowerCase();
++ const baseb = basename(b).toLowerCase();
++
++ return exta.localeCompare(extb, 'en') ||
++ basea.localeCompare(baseb, 'en') ||
++ a.localeCompare(b, 'en')
++ }
++
++ // convenience method: this joins the given rules with newlines, appends a trailing newline,
++ // and calls the internal onReadIgnoreFile method
++ injectRules (filename, rules, callback = () => {}) {
++ this.onReadIgnoreFile(filename, `${rules.join('\n')}\n`, callback);
++ }
++
++ // custom method: this is called by addIgnoreFile when we find a package.json, it uses the
++ // arborist tree to pull both default rules and strict rules for the package
++ processPackage (callback) {
++ const {
++ bin,
++ browser,
++ files,
++ main,
++ } = this.tree.package;
++
++ // rules in these arrays are inverted since they are patterns we want to _not_ ignore
++ const ignores = [];
++ const strict = [
++ ...strictDefaults,
++ '!/package.json',
++ '!/readme{,.*[^~$]}',
++ '!/copying{,.*[^~$]}',
++ '!/license{,.*[^~$]}',
++ '!/licence{,.*[^~$]}',
++ '/.git',
++ '/node_modules',
++ '.npmrc',
++ '/package-lock.json',
++ '/yarn.lock',
++ '/pnpm-lock.yaml',
++ ];
++
++ // if we have a files array in our package, we need to pull rules from it
++ if (files) {
++ for (let file of files) {
++ // invert the rule because these are things we want to include
++ if (file.startsWith('./')) {
++ file = file.slice(1);
++ }
++ if (file.endsWith('/*')) {
++ file += '*';
++ }
++ const inverse = `!${file}`;
++ try {
++ // if an entry in the files array is a specific file, then we need to include it as a
++ // strict requirement for this package. if it's a directory or a pattern, it's a default
++ // pattern instead. this is ugly, but we have to stat to find out if it's a file
++ const stat = lstat(join(this.path, file.replace(/^!+/, '')).replace(/\\/g, '/'));
++ // if we have a file and we know that, it's strictly required
++ if (stat.isFile()) {
++ strict.unshift(inverse);
++ this.requiredFiles.push(file.startsWith('/') ? file.slice(1) : file);
++ } else if (stat.isDirectory()) {
++ // otherwise, it's a default ignore, and since we got here we know it's not a pattern
++ // so we include the directory contents
++ ignores.push(inverse);
++ ignores.push(`${inverse}/**`);
++ }
++ // if the thing exists, but is neither a file or a directory, we don't want it at all
++ } catch (err) {
++ // if lstat throws, then we assume we're looking at a pattern and treat it as a default
++ ignores.push(inverse);
++ }
++ }
++
++ // we prepend a '*' to exclude everything, followed by our inverted file rules
++ // which now mean to include those
++ this.injectRules('package.json', ['*', ...ignores]);
++ }
++
++ // browser is required
++ if (browser) {
++ strict.push(`!/${browser}`);
++ }
++
++ // main is required
++ if (main) {
++ strict.push(`!/${main}`);
++ }
++
++ // each bin is required
++ if (bin) {
++ for (const key in bin) {
++ strict.push(`!/${bin[key]}`);
++ }
++ }
++
++ // and now we add all of the strict rules to our synthetic file
++ this.injectRules(strictRules, strict, callback);
++ }
++
++ // custom method: after we've finished gathering the files for the root package, we call this
++ // before emitting the 'done' event in order to gather all of the files for bundled deps
++ async gatherBundles () {
++ if (this.seen.has(this.tree)) {
++ return
++ }
++
++ // add this node to our seen tracker
++ this.seen.add(this.tree);
++
++ // if we're the project root, then we look at our bundleDependencies, otherwise we got here
++ // because we're a bundled dependency of the root, which means we need to include all prod
++ // and optional dependencies in the bundle
++ let toBundle;
++ if (this.tree.isProjectRoot) {
++ const { bundleDependencies } = this.tree.package;
++ toBundle = bundleDependencies || [];
++ } else {
++ const { dependencies, optionalDependencies } = this.tree.package;
++ toBundle = Object.keys(dependencies || {}).concat(Object.keys(optionalDependencies || {}));
++ }
++
++ for (const dep of toBundle) {
++ const edge = this.tree.edgesOut.get(dep);
++ // no edgeOut = missing node, so skip it. we can't pack it if it's not here
++ // we also refuse to pack peer dependencies and dev dependencies
++ if (!edge || edge.peer || edge.dev) {
++ continue
++ }
++
++ // get a reference to the node we're bundling
++ const node = this.tree.edgesOut.get(dep).to;
++ // if there's no node, this is most likely an optional dependency that hasn't been
++ // installed. just skip it.
++ if (!node) {
++ continue
++ }
++ // we use node.path for the path because we want the location the node was linked to,
++ // not where it actually lives on disk
++ const path = node.path;
++ // but link nodes don't have edgesOut, so we need to pass in the target of the node
++ // in order to make sure we correctly traverse its dependencies
++ const tree = node.target;
++
++ // and start building options to be passed to the walker for this package
++ const walkerOpts = {
++ path,
++ isPackage: true,
++ ignoreFiles: [],
++ seen: this.seen, // pass through seen so we can prevent infinite circular loops
++ };
++
++ // if our node is a link, we apply defaultRules. we don't do this for regular bundled
++ // deps because their .npmignore and .gitignore files are excluded by default and may
++ // override defaults
++ if (node.isLink) {
++ walkerOpts.ignoreFiles.push(defaultRules);
++ }
++
++ // _all_ nodes will follow package.json rules from their package root
++ walkerOpts.ignoreFiles.push('package.json');
++
++ // only link nodes will obey .npmignore or .gitignore
++ if (node.isLink) {
++ walkerOpts.ignoreFiles.push('.npmignore');
++ walkerOpts.ignoreFiles.push('.gitignore');
++ }
++
++ // _all_ nodes follow strict rules
++ walkerOpts.ignoreFiles.push(strictRules);
++
++ // create a walker for this dependency and gather its results
++ const walker = new PackWalker(tree, walkerOpts);
++ const bundled = await new Promise((pResolve, pReject) => {
++ walker.on('error', pReject);
++ walker.on('done', pResolve);
++ walker.start();
++ });
++
++ // now we make sure we have our paths correct from the root, and accumulate everything into
++ // our own result set to deduplicate
++ const relativeFrom = relative(this.root, walker.path);
++ for (const file of bundled) {
++ this.result.add(join(relativeFrom, file).replace(/\\/g, '/'));
++ }
++ }
++ }
++}
++
++const walk = (tree, options, callback) => {
++ if (typeof options === 'function') {
++ callback = options;
++ options = {};
++ }
++ const p = new Promise((pResolve, pReject) => {
++ new PackWalker(tree, { ...options, isPackage: true })
++ .on('done', pResolve).on('error', pReject).start();
++ });
++ return callback ? p.then(res => callback(null, res), callback) : p
++};
++
++var lib = walk;
++walk.Walker = PackWalker;
++
++var packlist = /*@__PURE__*/getDefaultExportFromCjs(lib);
++
++const edgesOut = /* @__PURE__ */ new Map();
++const getNpmPacklist = (absoluteLinkPackagePath, packageJson) => packlist({
++ path: absoluteLinkPackagePath,
++ package: packageJson,
++ // @ts-expect-error outdated types
++ edgesOut
++});
++
++const cwd = process.cwd();
++const cwdPath = (filePath) => path$2.relative(cwd, filePath);
++
++const getPrettyTime = () => (/* @__PURE__ */ new Date()).toLocaleTimeString(
++ void 0,
++ {
++ hour: "numeric",
++ minute: "numeric",
++ second: "numeric",
++ hour12: true
++ }
++);
++
++const waitFor = (test, interval, maxTimeout, errorMessage) => new Promise(async (resolve, reject) => {
++ const startTime = Date.now();
++ let attempts = 0;
++ const maxAttempts = Math.floor(maxTimeout / interval);
++ const attempt = async () => {
++ attempts++;
++ try {
++ const result = await test();
++ if (result) {
++ return resolve();
++ }
++ throw new Error();
++ } catch (error) {
++ const numAttemptsRemaining = maxAttempts - attempts;
++ console.error(red(` \u{1F615} Error: ${errorMessage}`), " retrying in", yellow(`${interval}ms`), ".", yellow(`${numAttemptsRemaining} attempts remaining`));
++ }
++ if (Date.now() - startTime >= maxTimeout) {
++ console.error(red(` \u{1F635} Error: ${errorMessage}. Giving up after ${maxAttempts} attempts`));
++ return reject();
++ }
++ setTimeout(attempt, interval);
++ };
++ await attempt();
++});
++
++const hardlinkPackage = async (linkPath, absoluteLinkPackagePath, packageJson, publishFilesPromise = getNpmPacklist(
++ absoluteLinkPackagePath,
++ packageJson
++), interval = 500, maxBuildTime = 3e4) => {
++ const [oldPublishFiles, publishFiles] = await Promise.all([
++ getNpmPacklist(
++ linkPath,
++ /**
++ * This is evaluated in the context of the new package.json since that
++ * defines which files belong to the package.
++ */
++ packageJson
++ ),
++ publishFilesPromise
++ ]);
++ console.log(`Linking ${magenta(packageJson.name)} in publish mode:`);
++ await Promise.all(publishFiles.map(async (file) => {
++ const sourcePath = path$2.join(absoluteLinkPackagePath, file);
++ await waitFor(
++ async () => await fsExists(sourcePath),
++ interval,
++ maxBuildTime,
++ ""
++ );
++ }));
++ await Promise.all(
++ publishFiles.map(async (file) => {
++ const sourcePath = path$2.join(absoluteLinkPackagePath, file);
++ const targetPath = path$2.join(linkPath, file);
++ await fs$2.mkdir(
++ path$2.dirname(targetPath),
++ { recursive: true }
++ );
++ try {
++ await hardlink(sourcePath, targetPath);
++ } catch (error) {
++ console.warn(
++ ` ${red("\u2716 Failed to link")}`,
++ cyan(cwdPath(targetPath)),
++ "\u2192",
++ cyan(cwdPath(sourcePath)),
++ error.message ?? error
++ );
++ return;
++ }
++ const fileIndex = oldPublishFiles.indexOf(file);
++ if (fileIndex > -1) {
++ oldPublishFiles.splice(fileIndex, 1);
++ }
++ console.log(
++ ` ${green("\u2714")}`,
++ cyan(cwdPath(targetPath)),
++ "\u2192",
++ cyan(cwdPath(sourcePath))
++ );
++ })
++ );
++ await Promise.all(
++ oldPublishFiles.map(async (file) => {
++ console.log(cyan(` \u{1F6AE} ${file} no longer in publish list, deleting it. If you did not intend to do this, something probably went wrong. See https://github.com/privatenumber/link?tab=readme-ov-file#publish-mode`));
++ await fs$2.rm(path$2.join(linkPath, file), {
++ force: true
++ });
++ })
++ );
++};
++
++const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
++
++const isValidSetup = async (linkPath, expectedPrefix) => {
++ const linkPathStat = await fs$2.stat(linkPath).catch(() => null);
++ if (!linkPathStat?.isDirectory()) {
++ return false;
++ }
++ const linkPathReal = await fs$2.realpath(linkPath);
++ return linkPathReal.startsWith(expectedPrefix);
++};
++const linkPublishMode = async (basePackagePath, linkPackagePath, watchMode, litmus, delay = 2e3, interval = 500, maxBuildTime = 3e4) => {
++ const absoluteLinkPackagePath = path$2.resolve(basePackagePath, linkPackagePath);
++ const packageJson = await readPackageJson(absoluteLinkPackagePath);
++ const expectedPrefix = path$2.join(basePackagePath, "node_modules/");
++ const linkPath = path$2.join(expectedPrefix, packageJson.name);
++ if (!await isValidSetup(linkPath, expectedPrefix)) {
++ console.error(
++ defaultOutdent`
++ Error: Package ${magenta(packageJson.name)} is not set up
++
++ ${bold("Setup instructions")}
++ 1. In the Dependency package, create a tarball:
++ ${dim("$ npm pack")}
++
++ 2. In the Consuming package, install the tarball and link the Dependency:
++ ${dim("$ npm install --no-save ")}
++ ${dim("$ npx link publish ")}
++
++ 3. Start developing!
++
++ Learn more: https://npmjs.com/link
++ `
++ );
++ return;
++ }
++ const debouncedSleepForDelay = pDebounce(sleep, delay);
++ const debouncedHardlinkPackage = debounce$1(hardlinkPackage, delay);
++ await hardlinkPackage(
++ linkPath,
++ absoluteLinkPackagePath,
++ packageJson
++ );
++ if (watchMode) {
++ const globOptions = {
++ globstar: true,
++ extended: true
++ };
++ const ignoreFiles = [
++ // Files
++ "**/{npm-debug.log,*.orig,package-lock.json,yarn.lock,pnpm-lock.yaml}",
++ // Folders
++ "**/node_modules/**",
++ // Hidden files
++ "**/.{_*,*.swp,DS_Store,gitignore,npmrc,npmignore,lock-wscript,.wafpickle-*}",
++ // Hidden folders
++ "**/.{_*,git,svn,hg,CVS}/**"
++ ].map((glob) => globToRegexp$1(glob, globOptions));
++ const watcher = fs$2.watch(
++ absoluteLinkPackagePath,
++ { recursive: true }
++ );
++ for await (const { eventType, filename } of watcher) {
++ if (!filename) {
++ continue;
++ }
++ const shouldIgnore = ignoreFiles.some((ignoreFile) => ignoreFile.test(filename));
++ if (shouldIgnore) {
++ continue;
++ }
++ await debouncedSleepForDelay(delay);
++ if (litmus) {
++ await waitFor(
++ async () => fsExists(path$2.join(absoluteLinkPackagePath, litmus)),
++ interval,
++ maxBuildTime,
++ ""
++ );
++ }
++ const publishFiles = await getNpmPacklist(
++ absoluteLinkPackagePath,
++ packageJson
++ );
++ if (!publishFiles.includes(filename)) {
++ continue;
++ }
++ console.log(`
++${dim(getPrettyTime())}`, "Detected", yellow(eventType), "in", `${cyan(cwdPath(path$2.join(absoluteLinkPackagePath, filename)))}
++`);
++ await debouncedHardlinkPackage(
++ linkPath,
++ absoluteLinkPackagePath,
++ packageJson,
++ publishFiles
++ );
++ }
++ }
++};
++
++const publishCommand = G({
++ name: "publish",
++ parameters: [""],
++ flags: {
++ watch: {
++ type: Boolean,
++ alias: "w",
++ description: "Watch for changes in the package and automatically relink"
++ },
++ litmus: {
++ type: String,
++ alias: "l",
++ description: "If using the --watch flag, look for this file in the linked package to see if it's ready to re-link"
++ },
++ delay: {
++ type: Number,
++ alias: "d",
++ description: "If using the --watch flag without the litmus flag, wait this amount of time (in ms) after detecting changes before refreshing the packlist and re-linking",
++ default: 2e3
++ },
++ interval: {
++ type: Number,
++ alias: "i",
++ description: "If using the --watch flag, poll for completed builds at this frequency (in ms)",
++ default: 500
++ },
++ maxBuildTime: {
++ type: Number,
++ alias: "m",
++ description: "If using the --watch flag, the maximum amount of time to wait for all expected files to appear before re-linking",
++ default: 3e4
++ }
++ },
++ help: {
++ description: "Link a package to simulate an environment similar to `npm install`"
++ }
++});
++const publishHandler = async (cwdProjectPath, packagePaths, flags) => {
++ if (packagePaths.length > 0) {
++ await Promise.all(
++ packagePaths.map(
++ (linkPackagePath) => linkPublishMode(
++ cwdProjectPath,
++ linkPackagePath,
++ flags.watch,
++ flags.litmus,
++ flags.delay,
++ flags.interval,
++ flags.maxBuildTime
++ )
++ )
++ );
++ }
++};
++
++(async () => {
++ const argv = Z({
++ name: "link",
++ parameters: ["[package paths...]"],
++ flags: {
++ deep: {
++ type: Boolean,
++ alias: "d",
++ description: "Run `npx link` on dependencies if they have a link.config.json"
++ }
++ },
++ help: {
++ description: "A better `npm link` -- symlink local dependencies to the current project",
++ render: (nodes, renderers) => {
++ nodes[0].data = "npx link\n";
++ nodes.splice(2, 0, {
++ type: "section",
++ data: {
++ title: "Website",
++ body: "https://www.npmjs.com/package/link"
++ }
++ });
++ return renderers.render(nodes);
++ }
++ },
++ commands: [
++ publishCommand
++ ]
++ });
++ const cwdProjectPath = await fs$1.realpath(process.cwd());
++ if (!argv.command) {
++ const { packagePaths } = argv._;
++ if (packagePaths.length > 0) {
++ await Promise.all(
++ packagePaths.map(
++ (linkPackagePath) => linkPackage(
++ cwdProjectPath,
++ linkPackagePath,
++ argv.flags
++ )
++ )
++ );
++ return;
++ }
++ const config = await loadConfig(cwdProjectPath);
++ if (!config) {
++ console.warn(
++ defaultOutdent`
+ Warning: Config file "link.config.json" not found in current directory.
+ Read the documentation to learn more: https://www.npmjs.com/package/link
+- `),u.showHelp();return}await Vu(e,D,{deep:u.flags.deep})}})().catch(u=>{console.error("Error:",u.message),process.exit(1)});
++ `
++ );
++ argv.showHelp();
++ return;
++ }
++ await linkFromConfig(
++ cwdProjectPath,
++ config,
++ {
++ deep: argv.flags.deep
++ }
++ );
++ } else if (argv.command === "publish") {
++ await publishHandler(
++ cwdProjectPath,
++ argv._,
++ argv.flags
++ );
++ }
++})().catch((error) => {
++ console.error("Error:", error.message);
++ process.exit(1);
++});
+diff --git a/node_modules/link/dist/libs/fsevents.node b/node_modules/link/dist/libs/fsevents.node
+new file mode 100755
+index 0000000..1cc3345
+Binary files /dev/null and b/node_modules/link/dist/libs/fsevents.node differ
diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch
index cc9c8531e3a3..885faec875bc 100644
--- a/patches/react-native-modal+13.0.1.patch
+++ b/patches/react-native-modal+13.0.1.patch
@@ -11,10 +11,22 @@ index b63bcfc..bd6419e 100644
buildPanResponder: () => void;
getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number;
diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js
-index 80f4e75..5a58eae 100644
+index 80f4e75..602cdff 100644
--- a/node_modules/react-native-modal/dist/modal.js
+++ b/node_modules/react-native-modal/dist/modal.js
-@@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component {
+@@ -59,6 +59,11 @@ export class ReactNativeModal extends React.Component {
+ deviceHeight: Dimensions.get('window').height,
+ isSwipeable: !!this.props.swipeDirection,
+ pan: null,
++ backdrop: {
++ prevOpacity: 0,
++ opacity: 0,
++ },
++ contentAnimation: {},
+ };
+ this.isTransitioning = false;
+ this.inSwipeClosingState = false;
+@@ -75,6 +80,13 @@ export class ReactNativeModal extends React.Component {
}
return false;
};
@@ -28,7 +40,202 @@ index 80f4e75..5a58eae 100644
this.shouldPropagateSwipe = (evt, gestureState) => {
return typeof this.props.propagateSwipe === 'function'
? this.props.propagateSwipe(evt, gestureState)
-@@ -453,10 +460,18 @@ export class ReactNativeModal extends React.Component {
+@@ -134,10 +146,12 @@ export class ReactNativeModal extends React.Component {
+ if (this.isSwipeDirectionAllowed(gestureState)) {
+ // Dim the background while swiping the modal
+ const newOpacityFactor = 1 - this.calcDistancePercentage(gestureState);
+- this.backdropRef &&
+- this.backdropRef.transitionTo({
+- opacity: this.props.backdropOpacity * newOpacityFactor,
+- });
++ this.setState((prevState) => ({
++ backdrop: {
++ prevOpacity: prevState.backdrop.opacity,
++ opacity: newOpacityFactor * this.props.backdropOpacity,
++ }
++ }))
+ animEvt(evt, gestureState);
+ if (this.props.onSwipeMove) {
+ this.props.onSwipeMove(newOpacityFactor, gestureState);
+@@ -185,11 +199,13 @@ export class ReactNativeModal extends React.Component {
+ if (this.props.onSwipeCancel) {
+ this.props.onSwipeCancel(gestureState);
+ }
+- if (this.backdropRef) {
+- this.backdropRef.transitionTo({
++ this.setState((prevState) => ({
++ backdrop: {
++ prevOpacity: prevState.backdrop.opacity,
+ opacity: this.props.backdropOpacity,
+- });
+- }
++ duration: undefined,
++ }
++ }))
+ Animated.spring(this.state.pan, {
+ toValue: { x: 0, y: 0 },
+ bounciness: 0,
+@@ -300,40 +316,53 @@ export class ReactNativeModal extends React.Component {
+ }
+ }
+ };
++ this.onContentAnimationEnd = () => {
++ this.isTransitioning = false;
++
++ if (this.interactionHandle) {
++ InteractionManager.clearInteractionHandle(this.interactionHandle);
++ this.interactionHandle = null;
++ }
++ if (!this.props.isVisible) {
++ this.setState({
++ showContent: false,
++ }, () => {
++ this.setState({
++ isVisible: false,
++ }, () => {
++ this.props.onModalHide();
++ });
++ });
++ } else {
++ this.props.onModalShow();
++ }
++ }
+ this.open = () => {
+ if (this.isTransitioning) {
+ return;
+ }
+ this.isTransitioning = true;
+- if (this.backdropRef) {
+- this.backdropRef.transitionTo({ opacity: this.props.backdropOpacity }, this.props.backdropTransitionInTiming);
+- }
++
++ this.setState((prevState) => ({
++ backdrop: {
++ prevOpacity: prevState.backdrop.opacity,
++ opacity: this.props.backdropOpacity,
++ duration: this.props.backdropTransitionInTiming,
++ },
++ contentAnimation: {
++ animation: this.animationIn,
++ duration: this.props.animationInTiming,
++ }
++ }))
+ // This is for resetting the pan position,otherwise the modal gets stuck
+ // at the last released position when you try to open it.
+ // TODO: Could certainly be improved - no idea for the moment.
+ if (this.state.isSwipeable) {
+ this.state.pan.setValue({ x: 0, y: 0 });
+ }
+- if (this.contentRef) {
+- this.props.onModalWillShow && this.props.onModalWillShow();
+- if (this.interactionHandle == null) {
+- this.interactionHandle = InteractionManager.createInteractionHandle();
+- }
+- this.contentRef
+- .animate(this.animationIn, this.props.animationInTiming)
+- .then(() => {
+- this.isTransitioning = false;
+- if (this.interactionHandle) {
+- InteractionManager.clearInteractionHandle(this.interactionHandle);
+- this.interactionHandle = null;
+- }
+- if (!this.props.isVisible) {
+- this.close();
+- }
+- else {
+- this.props.onModalShow();
+- }
+- });
++ this.props.onModalWillShow && this.props.onModalWillShow();
++ if (this.interactionHandle === null) {
++ this.interactionHandle = InteractionManager.createInteractionHandle();
+ }
+ };
+ this.close = () => {
+@@ -341,9 +370,6 @@ export class ReactNativeModal extends React.Component {
+ return;
+ }
+ this.isTransitioning = true;
+- if (this.backdropRef) {
+- this.backdropRef.transitionTo({ opacity: 0 }, this.props.backdropTransitionOutTiming);
+- }
+ let animationOut = this.animationOut;
+ if (this.inSwipeClosingState) {
+ this.inSwipeClosingState = false;
+@@ -360,35 +386,22 @@ export class ReactNativeModal extends React.Component {
+ animationOut = 'slideOutLeft';
+ }
+ }
+- if (this.contentRef) {
+- this.props.onModalWillHide && this.props.onModalWillHide();
+- if (this.interactionHandle == null) {
+- this.interactionHandle = InteractionManager.createInteractionHandle();
+- }
+- this.contentRef
+- .animate(animationOut, this.props.animationOutTiming)
+- .then(() => {
+- this.isTransitioning = false;
+- if (this.interactionHandle) {
+- InteractionManager.clearInteractionHandle(this.interactionHandle);
+- this.interactionHandle = null;
+- }
+- if (this.props.isVisible) {
+- this.open();
+- }
+- else {
+- this.setState({
+- showContent: false,
+- }, () => {
+- this.setState({
+- isVisible: false,
+- }, () => {
+- this.props.onModalHide();
+- });
+- });
+- }
+- });
++ this.props.onModalWillHide && this.props.onModalWillHide();
++ if (this.interactionHandle == null) {
++ this.interactionHandle = InteractionManager.createInteractionHandle();
+ }
++
++ this.setState((prevState) => ({
++ backdrop: {
++ prevOpacity: prevState.backdrop.opacity,
++ opacity: 0,
++ duration: this.props.backdropTransitionOutTiming,
++ },
++ contentAnimation: {
++ animation: animationOut,
++ duration: this.props.animationOutTiming,
++ }
++ }))
+ };
+ this.makeBackdrop = () => {
+ if (!this.props.hasBackdrop) {
+@@ -409,9 +422,20 @@ export class ReactNativeModal extends React.Component {
+ : 'transparent',
+ },
+ ];
++ const animation = this.state.backdrop.opacity !== this.state.backdrop.prevOpacity ? {
++ from: {
++ opacity: this.state.backdrop.prevOpacity,
++ },
++ to: {
++ opacity: this.state.backdrop.opacity,
++ }
++ } : undefined;
+ const backdropWrapper = (React.createElement(animatable.View, { ref: ref => (this.backdropRef = ref), useNativeDriver: useNativeDriverForBackdrop !== undefined
+ ? useNativeDriverForBackdrop
+- : useNativeDriver, style: [styles.backdrop, backdropComputedStyle] }, hasCustomBackdrop && customBackdrop));
++ : useNativeDriver,
++ duration: this.state.backdrop.duration,
++ animation,
++ style: [styles.backdrop, backdropComputedStyle] }, hasCustomBackdrop && customBackdrop));
+ if (hasCustomBackdrop) {
+ // The user will handle backdrop presses himself
+ return backdropWrapper;
+@@ -453,10 +477,18 @@ export class ReactNativeModal extends React.Component {
if (this.state.isVisible) {
this.open();
}
@@ -48,7 +255,7 @@ index 80f4e75..5a58eae 100644
if (this.didUpdateDimensionsEmitter) {
this.didUpdateDimensionsEmitter.remove();
}
-@@ -464,6 +479,9 @@ export class ReactNativeModal extends React.Component {
+@@ -464,6 +496,9 @@ export class ReactNativeModal extends React.Component {
InteractionManager.clearInteractionHandle(this.interactionHandle);
this.interactionHandle = null;
}
@@ -58,7 +265,40 @@ index 80f4e75..5a58eae 100644
}
componentDidUpdate(prevProps) {
// If the animations have been changed then rebuild them to make sure we're
-@@ -525,7 +543,7 @@ export class ReactNativeModal extends React.Component {
+@@ -475,9 +510,14 @@ export class ReactNativeModal extends React.Component {
+ this.animationOut = animationOut;
+ }
+ // If backdrop opacity has been changed then make sure to update it
+- if (this.props.backdropOpacity !== prevProps.backdropOpacity &&
+- this.backdropRef) {
+- this.backdropRef.transitionTo({ opacity: this.props.backdropOpacity }, this.props.backdropTransitionInTiming);
++ if (this.props.backdropOpacity !== prevProps.backdropOpacity) {
++ this.setState((prevState) => ({
++ backdrop: {
++ prevOpacity: prevState.backdrop.opacity,
++ opacity: this.props.backdropOpacity,
++ duration: this.props.backdropTransitionInTiming,
++ }
++ }))
+ }
+ // On modal open request, we slide the view up and fade in the backdrop
+ if (this.props.isVisible && !prevProps.isVisible) {
+@@ -515,7 +555,13 @@ export class ReactNativeModal extends React.Component {
+ const _children = this.props.hideModalContentWhileAnimating &&
+ this.props.useNativeDriver &&
+ !this.state.showContent ? (React.createElement(animatable.View, null)) : (children);
+- const containerView = (React.createElement(animatable.View, Object.assign({}, panHandlers, { ref: ref => (this.contentRef = ref), style: [panPosition, computedStyle], pointerEvents: "box-none", useNativeDriver: useNativeDriver }, containerProps), _children));
++ const containerView = (React.createElement(animatable.View, Object.assign({}, panHandlers, {
++ ref: ref => (this.contentRef = ref), style: [panPosition, computedStyle],
++ pointerEvents: "box-none", useNativeDriver: useNativeDriver,
++ animation: this.state.contentAnimation.animation,
++ duration: this.state.contentAnimation.duration,
++ onAnimationEnd: this.onContentAnimationEnd,
++ }, containerProps), _children));
+ // If coverScreen is set to false by the user
+ // we render the modal inside the parent view directly
+ if (!coverScreen && this.state.isVisible) {
+@@ -525,7 +571,7 @@ export class ReactNativeModal extends React.Component {
}
return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps),
this.makeBackdrop(),
diff --git a/src/CONST.ts b/src/CONST.ts
index 9d7f245b5464..2d1396e7bfa6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -86,6 +86,7 @@ const CONST = {
DEFAULT_TABLE_NAME: 'keyvaluepairs',
DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt',
DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL],
+ DISABLED_MAX_EXPENSE_VALUE: 10000000000,
// Note: Group and Self-DM excluded as these are not tied to a Workspace
WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT],
@@ -355,6 +356,7 @@ const CONST = {
OPEN: 'OPEN',
},
MAX_LENGTH: {
+ FULL_SSN: 9,
SSN: 4,
ZIP_CODE: 10,
},
@@ -381,6 +383,7 @@ const CONST = {
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
WORKSPACE_FEEDS: 'workspaceFeeds',
NETSUITE_USA_TAX: 'netsuiteUsaTax',
+ WORKSPACE_RULES: 'workspaceRules',
},
BUTTON_STATES: {
DEFAULT: 'default',
@@ -708,6 +711,7 @@ const CONST = {
FORWARDED: 'FORWARDED', // OldDot Action
HOLD: 'HOLD',
HOLD_COMMENT: 'HOLDCOMMENT',
+ INTEGRATION_SYNC_FAILED: 'INTEGRATIONSYNCFAILED',
IOU: 'IOU',
INTEGRATIONS_MESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action
MANAGER_ATTACH_RECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action
@@ -972,10 +976,11 @@ const CONST = {
HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render',
REPORT_INITIAL_RENDER: 'report_initial_render',
SWITCH_REPORT: 'switch_report',
- SWITCH_REPORT_FROM_PREVIEW: 'switch_report_from_preview',
- SWITCH_REPORT_THREAD: 'switch_report_thread',
+ OPEN_REPORT_FROM_PREVIEW: 'open_report_from_preview',
+ OPEN_REPORT_THREAD: 'open_report_thread',
SIDEBAR_LOADED: 'sidebar_loaded',
LOAD_SEARCH_OPTIONS: 'load_search_options',
+ MESSAGE_SENT: 'message_sent',
COLD: 'cold',
WARM: 'warm',
REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500,
@@ -1188,6 +1193,7 @@ const CONST = {
VISIBLE_PASSWORD: 'visible-password',
ASCII_CAPABLE: 'ascii-capable',
NUMBER_PAD: 'number-pad',
+ DECIMAL_PAD: 'decimal-pad',
},
INPUT_MODE: {
@@ -2079,9 +2085,11 @@ const CONST = {
ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled',
ARE_REPORT_FIELDS_ENABLED: 'areReportFieldsEnabled',
ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled',
+ ARE_COMPANY_CARDS_ENABLED: 'areCompanyCardsEnabled',
ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled',
ARE_INVOICES_ENABLED: 'areInvoicesEnabled',
ARE_TAXES_ENABLED: 'tax',
+ ARE_RULES_ENABLED: 'areRulesEnabled',
},
DEFAULT_CATEGORIES: [
'Advertising',
@@ -2421,9 +2429,11 @@ const CONST = {
WORKSPACE_MEMBERS: 'WorkspaceManageMembers',
WORKSPACE_EXPENSIFY_CARD: 'WorkspaceExpensifyCard',
WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows',
+ WORKSPACE_COMPANY_CARDS: 'WorkspaceCompanyCards',
WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount',
WORKSPACE_SETTINGS: 'WorkspaceSettings',
WORKSPACE_FEATURES: 'WorkspaceFeatures',
+ WORKSPACE_RULES: 'WorkspaceRules',
},
get EXPENSIFY_EMAILS() {
return [
@@ -4175,7 +4185,7 @@ const CONST = {
VIDEO_PLAYER: {
POPOVER_Y_OFFSET: -30,
- PLAYBACK_SPEEDS: [0.25, 0.5, 1, 1.5, 2],
+ PLAYBACK_SPEEDS: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
HIDE_TIME_TEXT_WIDTH: 250,
MIN_WIDTH: 170,
MIN_HEIGHT: 120,
@@ -5471,6 +5481,22 @@ const CONST = {
description: 'workspace.upgrade.taxCodes.description' as const,
icon: 'Coins',
},
+ companyCards: {
+ id: 'companyCards' as const,
+ alias: 'company-cards',
+ name: 'Company Cards',
+ title: 'workspace.upgrade.companyCards.title' as const,
+ description: 'workspace.upgrade.companyCards.description' as const,
+ icon: 'CompanyCard',
+ },
+ rules: {
+ id: 'rules' as const,
+ alias: 'rules',
+ name: 'Rules',
+ title: 'workspace.upgrade.rules.title' as const,
+ description: 'workspace.upgrade.rules.description' as const,
+ icon: 'Rules',
+ },
};
},
REPORT_FIELD_TYPES: {
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 620243440384..8a2ef4a2b2f4 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -256,6 +256,7 @@ function Expensify({
{shouldInit && (
<>
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index b7b6cf53a176..8d60a5b57511 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -458,6 +458,7 @@ const ONYXKEYS = {
// Shared NVPs
/** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */
SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_',
+ SHARED_NVP_PRIVATE_DOMAIN_MEMBER: 'sharedNVP_private_domain_member_',
/**
* Stores the card list for a given fundID and feed in the format: cards__
@@ -749,6 +750,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress;
[ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
+ [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CompanyCards;
[ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;
[ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 893fd59e38b4..47a2ad76209e 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -44,6 +44,7 @@ const ROUTES = {
SEARCH_ADVANCED_FILTERS_MERCHANT: 'search/filters/merchant',
SEARCH_ADVANCED_FILTERS_DESCRIPTION: 'search/filters/description',
SEARCH_ADVANCED_FILTERS_REPORT_ID: 'search/filters/reportID',
+ SEARCH_ADVANCED_FILTERS_AMOUNT: 'search/filters/amount',
SEARCH_ADVANCED_FILTERS_CATEGORY: 'search/filters/category',
SEARCH_ADVANCED_FILTERS_KEYWORD: 'search/filters/keyword',
SEARCH_ADVANCED_FILTERS_CARD: 'search/filters/card',
@@ -349,7 +350,7 @@ const ROUTES = {
},
ROOM_INVITE: {
route: 'r/:reportID/invite/:role?',
- getRoute: (reportID: string, role?: string) => `r/${reportID}/invite/${role}` as const,
+ getRoute: (reportID: string, role?: string) => `r/${reportID}/invite/${role ?? ''}` as const,
},
MONEY_REQUEST_HOLD_REASON: {
route: ':type/edit/reason/:transactionID?',
@@ -927,6 +928,14 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/expensify-card/settings/frequency',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/frequency` as const,
},
+ WORKSPACE_COMPANY_CARDS: {
+ route: 'settings/workspaces/:policyID/company-cards',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards` as const,
+ },
+ WORKSPACE_RULES: {
+ route: 'settings/workspaces/:policyID/rules',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules` as const,
+ },
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 30adc5f89d08..686a752ad360 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -37,6 +37,7 @@ const SCREENS = {
ADVANCED_FILTERS_DESCRIPTION_RHP: 'Search_Advanced_Filters_Description_RHP',
ADVANCED_FILTERS_MERCHANT_RHP: 'Search_Advanced_Filters_Merchant_RHP',
ADVANCED_FILTERS_REPORT_ID_RHP: 'Search_Advanced_Filters_ReportID_RHP',
+ ADVANCED_FILTERS_AMOUNT_RHP: 'Search_Advanced_Filters_Amount_RHP',
ADVANCED_FILTERS_CATEGORY_RHP: 'Search_Advanced_Filters_Category_RHP',
ADVANCED_FILTERS_KEYWORD_RHP: 'Search_Advanced_Filters_Keyword_RHP',
ADVANCED_FILTERS_CARD_RHP: 'Search_Advanced_Filters_Card_RHP',
@@ -363,6 +364,7 @@ const SCREENS = {
RATE_AND_UNIT: 'Workspace_RateAndUnit',
RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate',
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
+ COMPANY_CARDS: 'Workspace_CompanyCards',
EXPENSIFY_CARD: 'Workspace_ExpensifyCard',
EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details',
EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit',
@@ -440,6 +442,7 @@ const SCREENS = {
DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit',
DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit',
UPGRADE: 'Workspace_Upgrade',
+ RULES: 'Policy_Rules',
},
EDIT_REQUEST: {
diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx
new file mode 100644
index 000000000000..78b7c84ecb54
--- /dev/null
+++ b/src/components/AmountWithoutCurrencyForm.tsx
@@ -0,0 +1,66 @@
+import React, {useCallback, useMemo} from 'react';
+import type {ForwardedRef} from 'react';
+import useLocalize from '@hooks/useLocalize';
+import {addLeadingZero, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils';
+import CONST from '@src/CONST';
+import TextInput from './TextInput';
+import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types';
+
+type AmountFormProps = {
+ /** Amount supplied by the FormProvider */
+ value?: string;
+
+ /** Callback to update the amount in the FormProvider */
+ onInputChange?: (value: string) => void;
+} & Partial;
+
+function AmountWithoutCurrencyForm(
+ {value: amount, onInputChange, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps,
+ ref: ForwardedRef,
+) {
+ const {toLocaleDigit} = useLocalize();
+
+ const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]);
+
+ /**
+ * Sets the selection and the amount accordingly to the value passed to the input
+ * @param newAmount - Changed amount from user input
+ */
+ const setNewAmount = useCallback(
+ (newAmount: string) => {
+ // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
+ // More info: https://github.com/Expensify/App/issues/16974
+ const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
+ const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces);
+ const withLeadingZero = addLeadingZero(replacedCommasAmount);
+ if (!validateAmount(withLeadingZero, 2)) {
+ return;
+ }
+ onInputChange?.(withLeadingZero);
+ },
+ [onInputChange],
+ );
+
+ const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit);
+
+ return (
+
+ );
+}
+
+AmountWithoutCurrencyForm.displayName = 'AmountWithoutCurrencyForm';
+
+export default React.forwardRef(AmountWithoutCurrencyForm);
diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx
index b801743732bc..a2fcae901681 100644
--- a/src/components/Composer/index.native.tsx
+++ b/src/components/Composer/index.native.tsx
@@ -1,11 +1,12 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import type {ForwardedRef} from 'react';
-import React, {useCallback, useMemo, useRef} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native';
import {StyleSheet} from 'react-native';
import type {FileObject} from '@components/AttachmentModal';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -46,6 +47,15 @@ function Composer(
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const {inputCallbackRef, inputRef: autoFocusInputRef} = useAutoFocusInput();
+
+ useEffect(() => {
+ if (autoFocus === !!autoFocusInputRef.current) {
+ return;
+ }
+ inputCallbackRef(autoFocus ? textInput.current : null);
+ }, [autoFocus, inputCallbackRef, autoFocusInputRef]);
+
/**
* Set the TextInput Ref
* @param {Element} el
@@ -57,6 +67,10 @@ function Composer(
return;
}
+ if (autoFocus) {
+ inputCallbackRef(el);
+ }
+
// This callback prop is used by the parent component using the constructor to
// get a ref to the inner textInput element e.g. if we do
// this.textInput = el} /> this will not
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 3db92a2122b3..7a4512ad5aea 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -292,7 +292,7 @@ function Composer(
return;
}
- const currentText = textInput.current.innerText;
+ const currentText = textInput.current.value;
textInput.current.clear();
// We need to reset the selection to 0,0 manually after clearing the text input on web
diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx
index 649e66ccefa8..b395eb12c5fe 100644
--- a/src/components/DeeplinkWrapper/index.website.tsx
+++ b/src/components/DeeplinkWrapper/index.website.tsx
@@ -5,6 +5,7 @@ import Navigation from '@libs/Navigation/Navigation';
import navigationRef from '@libs/Navigation/navigationRef';
import shouldPreventDeeplinkPrompt from '@libs/Navigation/shouldPreventDeeplinkPrompt';
import * as App from '@userActions/App';
+import * as Link from '@userActions/Link';
import * as Session from '@userActions/Session';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
@@ -15,7 +16,7 @@ function isMacOSWeb(): boolean {
return !Browser.isMobile() && typeof navigator === 'object' && typeof navigator.userAgent === 'string' && /Mac/i.test(navigator.userAgent) && !/Electron/i.test(navigator.userAgent);
}
-function promptToOpenInDesktopApp() {
+function promptToOpenInDesktopApp(initialUrl = '') {
// If the current url path is /transition..., meaning it was opened from oldDot, during this transition period:
// 1. The user session may not exist, because sign-in has not been completed yet.
// 2. There may be non-idempotent operations (e.g. create a new workspace), which obviously should not be executed again in the desktop app.
@@ -26,11 +27,11 @@ function promptToOpenInDesktopApp() {
// Match any magic link (/v//<6 digit code>)
const isMagicLink = CONST.REGEX.ROUTES.VALIDATE_LOGIN.test(window.location.pathname);
- App.beginDeepLinkRedirect(!isMagicLink);
+ App.beginDeepLinkRedirect(!isMagicLink, Link.getInternalNewExpensifyPath(initialUrl));
}
}
-function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWrapperProps) {
+function DeeplinkWrapper({children, isAuthenticated, autoAuthState, initialUrl}: DeeplinkWrapperProps) {
const [currentScreen, setCurrentScreen] = useState();
const [hasShownPrompt, setHasShownPrompt] = useState(false);
const removeListener = useRef<() => void>();
@@ -77,7 +78,7 @@ function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWra
// Otherwise, we want to wait until the navigation state is set up
// and we know the user is on a screen that supports deeplinks.
if (isAuthenticated) {
- promptToOpenInDesktopApp();
+ promptToOpenInDesktopApp(initialUrl);
setHasShownPrompt(true);
} else {
// Navigation state is not set up yet, we're unsure if we should show the deep link prompt or not
@@ -93,7 +94,7 @@ function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWra
promptToOpenInDesktopApp();
setHasShownPrompt(true);
}
- }, [currentScreen, hasShownPrompt, isAuthenticated, autoAuthState]);
+ }, [currentScreen, hasShownPrompt, isAuthenticated, autoAuthState, initialUrl]);
return children;
}
diff --git a/src/components/DeeplinkWrapper/types.ts b/src/components/DeeplinkWrapper/types.ts
index db61e5b01c24..23e096d6a093 100644
--- a/src/components/DeeplinkWrapper/types.ts
+++ b/src/components/DeeplinkWrapper/types.ts
@@ -6,6 +6,8 @@ type DeeplinkWrapperProps = ChildrenProps & {
/** The auto authentication status */
autoAuthState?: string;
+
+ initialUrl?: string;
};
export default DeeplinkWrapperProps;
diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx
index a17cca5efae4..3c677a7b0f6d 100644
--- a/src/components/FeedbackSurvey.tsx
+++ b/src/components/FeedbackSurvey.tsx
@@ -43,9 +43,12 @@ type FeedbackSurveyProps = {
/** Indicates whether a loading indicator should be shown */
isLoading?: boolean;
+
+ /** Should the submit button be enabled when offline */
+ enabledWhenOffline?: boolean;
};
-function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired, isLoading, formID}: FeedbackSurveyProps) {
+function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired, isLoading, formID, enabledWhenOffline = true}: FeedbackSurveyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [draft, draftResults] = useOnyx(`${formID}Draft`);
@@ -103,7 +106,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe
onSubmit={handleSubmit}
submitButtonText={translate('common.submit')}
isSubmitButtonVisible={false}
- enabledWhenOffline
+ enabledWhenOffline={enabledWhenOffline}
>
{title}
@@ -138,7 +141,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe
onSubmit={handleSubmit}
message={translate('common.error.pleaseCompleteForm')}
buttonText={translate('common.submit')}
- enabledWhenOffline
+ enabledWhenOffline={enabledWhenOffline}
containerStyles={styles.mt3}
isLoading={isLoading}
/>
diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
index 82e7d4f30a85..686c318a99dc 100644
--- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
+++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
@@ -33,6 +33,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [
SCREENS.WORKSPACE.TAXES,
SCREENS.WORKSPACE.REPORT_FIELDS,
SCREENS.WORKSPACE.EXPENSIFY_CARD,
+ SCREENS.WORKSPACE.COMPANY_CARDS,
SCREENS.WORKSPACE.DISTANCE_RATES,
SCREENS.SEARCH.CENTRAL_PANE,
SCREENS.SETTINGS.TROUBLESHOOT,
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index ea33af302670..b1adf360bae6 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -82,6 +82,7 @@ import ExpensifyLogoNew from '@assets/images/expensify-logo-new.svg';
import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg';
import EyeDisabled from '@assets/images/eye-disabled.svg';
import Eye from '@assets/images/eye.svg';
+import Feed from '@assets/images/feed.svg';
import Filter from '@assets/images/filter.svg';
import Filters from '@assets/images/filters.svg';
import Flag from '@assets/images/flag.svg';
@@ -386,4 +387,5 @@ export {
Filters,
CalendarSolid,
Filter,
+ Feed,
};
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 9537e7a0a7a7..afce7f519ce5 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -51,7 +51,6 @@ import CheckmarkCircle from '@assets/images/simple-illustrations/simple-illustra
import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg';
import Coins from '@assets/images/simple-illustrations/simple-illustration__coins.svg';
import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg';
-import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__company-card.svg';
import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg';
import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg';
import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg';
@@ -87,6 +86,7 @@ import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustra
import ReceiptLocationMarker from '@assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg';
import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg';
import ReceiptUpload from '@assets/images/simple-illustrations/simple-illustration__receiptupload.svg';
+import Rules from '@assets/images/simple-illustrations/simple-illustration__rules.svg';
import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg';
import SendMoney from '@assets/images/simple-illustrations/simple-illustration__sendmoney.svg';
import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg';
@@ -101,6 +101,7 @@ import Tire from '@assets/images/simple-illustrations/simple-illustration__tire.
import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg';
import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg';
import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg';
+import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__twocards-horizontal.svg';
import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg';
import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg';
import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg';
@@ -216,4 +217,5 @@ export {
Tire,
BigVault,
Filters,
+ Rules,
};
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index b35b14016235..a734890a1f38 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -188,8 +188,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
);
const extraData = useMemo(
- () => [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale],
- [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale],
+ () => [reportActions, reports, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale],
+ [reportActions, reports, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale],
);
const previousOptionMode = usePrevious(optionMode);
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 32c1a3852c86..ee3929292cd3 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -106,6 +106,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);
const navigateBackToAfterDelete = useRef();
const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t));
+ const hasOnlyPendingTransactions = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).every(
+ (t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t),
+ );
const transactionIDs = allTransactions.map((t) => t.transactionID);
const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs);
const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID);
@@ -131,7 +134,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
- const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense;
+ const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions;
const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
const shouldShowAnyButton =
shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation || shouldShowExportIntegrationButton;
@@ -217,6 +220,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
if (allHavePendingRTERViolation) {
return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')};
}
+ if (hasOnlyPendingTransactions) {
+ return {icon: getStatusIcon(Expensicons.CreditCardHourglass), description: translate('iou.transactionPendingDescription')};
+ }
if (hasScanningReceipt) {
return {icon: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription')};
}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index ce6495e1385e..1350fd30ca54 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native';
import lodashIsEqual from 'lodash/isEqual';
import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx, withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebouncedState from '@hooks/useDebouncedState';
@@ -37,6 +37,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {SplitShares} from '@src/types/onyx/Transaction';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import type {DropdownOption} from './ButtonWithDropdownMenu/types';
import FormHelpMessage from './FormHelpMessage';
@@ -177,7 +178,7 @@ function MoneyRequestConfirmationList({
iouAmount,
policyCategories: policyCategoriesReal,
policyCategoriesDraft,
- mileageRates,
+ mileageRates: mileageRatesReal,
isDistanceRequest = false,
policy: policyReal,
policyDraft,
@@ -211,6 +212,10 @@ function MoneyRequestConfirmationList({
}: MoneyRequestConfirmationListProps) {
const policy = policyReal ?? policyDraft;
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
+ const [mileageRatesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, {
+ selector: (selectedPolicy: OnyxEntry) => DistanceRequestUtils.getMileageRates(selectedPolicy),
+ });
+ const mileageRates = isEmptyObject(mileageRatesReal) ? mileageRatesDraft : mileageRatesReal;
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
@@ -228,14 +233,15 @@ function MoneyRequestConfirmationList({
const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '-1';
useEffect(() => {
- if (customUnitRateID || !canUseP2PDistanceRequests) {
+ if ((customUnitRateID && customUnitRateID !== '-1') || !isDistanceRequest) {
return;
}
- if (!customUnitRateID) {
- const rateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultMileageRate?.customUnitRateID ?? '';
- IOU.setCustomUnitRateID(transactionID, rateID);
- }
- }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID]);
+
+ const defaultRate = defaultMileageRate?.customUnitRateID ?? '';
+ const lastSelectedRate = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate;
+ const rateID = canUseP2PDistanceRequests ? lastSelectedRate : defaultRate;
+ IOU.setCustomUnitRateID(transactionID, rateID);
+ }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID, isDistanceRequest]);
const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index a38845ec3362..4a76f33de346 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -19,7 +19,6 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useViolations from '@hooks/useViolations';
import type {ViolationField} from '@hooks/useViolations';
-import * as CardUtils from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import type {MileageRate} from '@libs/DistanceRequestUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -163,14 +162,14 @@ function MoneyRequestView({
tag: transactionTag,
originalAmount: transactionOriginalAmount,
originalCurrency: transactionOriginalCurrency,
- cardID: transactionCardID,
} = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]);
const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : '';
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
- const cardProgramName = isCardTransaction && transactionCardID !== undefined ? CardUtils.getCardDescription(transactionCardID) : '';
+ const cardProgramName = TransactionUtils.getCardName(transaction);
+ const shouldShowCard = isCardTransaction && cardProgramName;
const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
const isInvoice = ReportUtils.isInvoiceReport(moneyRequestReport);
const isPaidReport = ReportActionsUtils.isPayAction(parentReportAction);
@@ -187,7 +186,7 @@ function MoneyRequestView({
// Flags for allowing or disallowing editing an expense
// Used for non-restricted fields such as: description, category, tag, billable, etc...
- const canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(report);
+ const canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(report) && !readonly;
const canEdit = ReportActionsUtils.isMoneyRequestAction(parentReportAction) && ReportUtils.canEditMoneyRequest(parentReportAction, transaction) && canUserPerformWriteAction;
const canEditTaxFields = canEdit && !isDistanceRequest;
@@ -345,8 +344,8 @@ function MoneyRequestView({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
@@ -371,8 +370,8 @@ function MoneyRequestView({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))}
/>
@@ -429,8 +428,8 @@ function MoneyRequestView({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report?.reportID ?? '-1'))
@@ -502,7 +501,7 @@ function MoneyRequestView({
{shouldShowReceiptEmptyState && (
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
@@ -525,8 +524,8 @@ function MoneyRequestView({
titleIcon={Expensicons.Checkmark}
description={amountDescription}
titleStyle={styles.textHeadlineH2}
- interactive={canEditAmount && !readonly}
- shouldShowRightIcon={canEditAmount && !readonly}
+ interactive={canEditAmount}
+ shouldShowRightIcon={canEditAmount}
onPress={() =>
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
}
@@ -539,8 +538,8 @@ function MoneyRequestView({
description={translate('common.description')}
shouldParseTitle
title={updatedTransactionDescription ?? transactionDescription}
- interactive={canEdit && !readonly}
- shouldShowRightIcon={canEdit && !readonly}
+ interactive={canEdit}
+ shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() =>
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
@@ -558,8 +557,8 @@ function MoneyRequestView({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
@@ -575,8 +574,8 @@ function MoneyRequestView({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1' ?? '-1'))
@@ -590,8 +589,8 @@ function MoneyRequestView({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
@@ -602,7 +601,7 @@ function MoneyRequestView({
)}
{shouldShowTag && tagList}
- {isCardTransaction && (
+ {shouldShowCard && (
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
@@ -633,8 +632,8 @@ function MoneyRequestView({
Navigation.navigate(
@@ -672,7 +671,7 @@ function MoneyRequestView({
accessibilityLabel={translate('common.billable')}
isOn={updatedTransaction?.billable ?? !!transactionBillable}
onToggle={saveBillable}
- disabled={!canEdit || readonly}
+ disabled={!canEdit}
/>
)}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index dae33b438a43..45a06968cefd 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -365,7 +365,7 @@ function ReportPreview({
}
return {
supportText: translate('iou.expenseCount', {
- count: numberOfRequests - numberOfScanningReceipts - numberOfPendingRequests,
+ count: numberOfRequests,
scanningReceipts: numberOfScanningReceipts,
pendingReceipts: numberOfPendingRequests,
}),
@@ -389,7 +389,7 @@ function ReportPreview({
{
- Timing.start(CONST.TIMING.SWITCH_REPORT_FROM_PREVIEW);
+ Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID));
}}
onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index 7e4b69c39596..bae08155c262 100644
--- a/src/components/Search/SearchPageHeader.tsx
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -157,9 +157,12 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
}
const reportIDList = (selectedReports?.filter((report) => !!report) as string[]) ?? [];
- SearchActions.exportSearchItemsToCSV({query: status, reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, () => {
- setDownloadErrorModalOpen?.();
- });
+ SearchActions.exportSearchItemsToCSV(
+ {query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']},
+ () => {
+ setDownloadErrorModalOpen?.();
+ },
+ );
},
});
@@ -247,6 +250,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
return options;
}, [
+ queryJSON,
status,
selectedTransactionsKeys,
selectedTransactions,
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index bbc8615898df..7d8f4c1738c8 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -97,7 +97,7 @@ function BaseSelectionList(
shouldDelayFocus = true,
shouldUpdateFocusedIndex = false,
onLongPressRow,
- shouldShowListEmptyContent = false,
+ shouldShowListEmptyContent = true,
}: BaseSelectionListProps,
ref: ForwardedRef,
) {
diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
index bb2b2e24aea4..966f49e45a93 100644
--- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
+++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
@@ -15,7 +15,7 @@ const Context = React.createContext(null);
function VideoPopoverMenuContextProvider({children}: ChildrenProps) {
const {currentlyPlayingURL} = usePlaybackContext();
const {translate} = useLocalize();
- const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]);
+ const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[3]);
const {isOffline} = useNetwork();
const isLocalFile = currentlyPlayingURL && CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => currentlyPlayingURL.startsWith(prefix));
const videoPopoverMenuPlayerRef = useRef(null);
@@ -57,7 +57,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) {
text: translate('videoPlayer.playbackSpeed'),
subMenuItems: CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS.map((speed) => ({
icon: currentPlaybackSpeed === speed ? Expensicons.Checkmark : undefined,
- text: speed.toString(),
+ text: speed === 1 ? translate('videoPlayer.normal') : speed.toString(),
onSelected: () => {
updatePlaybackSpeed(speed);
},
diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts
index 022d6178877d..6199a36abdca 100644
--- a/src/hooks/useHtmlPaste/index.ts
+++ b/src/hooks/useHtmlPaste/index.ts
@@ -1,6 +1,5 @@
import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
-import type {ClipboardEvent as PasteEvent} from 'react';
import Parser from '@libs/Parser';
import type UseHtmlPaste from './types';
@@ -21,10 +20,6 @@ const insertAtCaret = (target: HTMLElement, text: string) => {
range.setEnd(node, node.length);
selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
- // Dispatch paste event to make Markdown Input properly set cursor position
- const pasteEvent = new ClipboardEvent('paste', {bubbles: true, cancelable: true});
- (pasteEvent as unknown as PasteEvent).isDefaultPrevented = () => false;
- target.dispatchEvent(pasteEvent);
// Dispatch input event to trigger Markdown Input to parse the new text
target.dispatchEvent(new Event('input', {bubbles: true}));
} else {
@@ -48,11 +43,6 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
insertByCommand(text);
}
- if (!textInputRef.current?.isFocused()) {
- textInputRef.current?.focus();
- return;
- }
-
// Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view.
// To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler
// We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered
diff --git a/src/languages/en.ts b/src/languages/en.ts
index bffbab74d13e..37b0c2f36c63 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -750,10 +750,17 @@ export default {
yourCompanyWebsiteNote: "If you don't have a website, you can provide your company's LinkedIn or social media profile instead.",
invalidDomainError: 'You have entered an invalid domain. To continue, please enter a valid domain.',
publicDomainError: 'You have entered a public domain. To continue, please enter a private domain.',
- expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) =>
- `${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${
- pendingReceipts > 0 ? `, ${pendingReceipts} pending` : ''
- }`,
+ expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => {
+ const expenseText = `${count} ${Str.pluralize('expense', 'expenses', count)}`;
+ const statusText = [];
+ if (scanningReceipts > 0) {
+ statusText.push(`${scanningReceipts} scanning`);
+ }
+ if (pendingReceipts > 0) {
+ statusText.push(`${pendingReceipts} pending`);
+ }
+ return statusText.length > 0 ? `${expenseText} (${statusText.join(', ')})` : expenseText;
+ },
deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Delete ${Str.pluralize('expense', 'expenses', count)}`,
deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Are you sure that you want to delete ${Str.pluralize('this expense', 'these expenses', count)}?`,
settledExpensify: 'Paid',
@@ -1427,7 +1434,7 @@ export default {
addPaymentMethod: 'Add payment method',
addNewDebitCard: 'Add new debit card',
addNewBankAccount: 'Add new bank account',
- accountLastFour: 'Account ending in',
+ accountLastFour: 'Ending in',
cardLastFour: 'Card ending in',
addFirstPaymentMethod: 'Add a payment method to send and receive payments directly in the app.',
defaultPaymentMethod: 'Default',
@@ -2105,6 +2112,7 @@ export default {
expensifyCard: 'Expensify Card',
workflows: 'Workflows',
workspace: 'Workspace',
+ companyCards: 'Company cards',
edit: 'Edit workspace',
enabled: 'Enabled',
disabled: 'Disabled',
@@ -2122,6 +2130,7 @@ export default {
travel: 'Travel',
members: 'Members',
accounting: 'Accounting',
+ rules: 'Rules',
displayedAs: 'Displayed as',
plan: 'Plan',
profile: 'Profile',
@@ -2585,7 +2594,7 @@ export default {
},
},
customersOrJobs: {
- title: 'Customers / projects',
+ title: 'Customers/projects',
subtitle: 'Choose how to handle NetSuite *customers* and *projects* in Expensify.',
importCustomers: 'Import customers',
importJobs: 'Import projects',
@@ -2600,7 +2609,7 @@ export default {
customSegments: {
title: 'Custom segments/records',
addText: 'Add custom segment/record',
- recordTitle: 'Custom segment',
+ recordTitle: 'Custom segment/record',
helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS,
helpLinkText: 'View detailed instructions',
helpText: ' on configuring custom segments/records.',
@@ -2831,6 +2840,10 @@ export default {
title: 'Spend',
subtitle: 'Enable functionality that helps you scale your team.',
},
+ manageSection: {
+ title: 'Manage',
+ subtitle: 'Add controls that help keep spend within budget.',
+ },
earnSection: {
title: 'Earn',
subtitle: 'Enable optional functionality to streamline your revenue and get paid faster.',
@@ -2864,6 +2877,13 @@ export default {
ctaTitle: 'Issue new card',
},
},
+ companyCards: {
+ title: 'Company Cards',
+ subtitle: 'Import spend from existing company cards',
+ disableCardTitle: 'Disable Company Cards',
+ disableCardPrompt: 'You can’t disable company cards because this feature is in use. Reach out to the Concierge for next steps.',
+ disableCardButton: 'Chat with Concierge',
+ },
workflows: {
title: 'Workflows',
subtitle: 'Configure how spend is approved and paid.',
@@ -2898,6 +2918,10 @@ export default {
disconnectText: "To disable accounting, you'll need to disconnect your accounting connection from your workspace.",
manageSettings: 'Manage settings',
},
+ rules: {
+ title: 'Rules',
+ subtitle: 'Configure when receipts are required, flag high spend, and more.',
+ },
},
reportFields: {
addField: 'Add field',
@@ -3543,6 +3567,16 @@ export default {
description: `Add tax codes to your taxes for easy export of expenses to your accounting and payroll systems.`,
onlyAvailableOnPlan: 'Tax codes are only available on the Control plan, starting at ',
},
+ companyCards: {
+ title: 'Company Cards',
+ description: `Company cards lets you import spend for existing company cards from all major card issuers. You can assign cards to employees, and automatically import transactions.`,
+ onlyAvailableOnPlan: 'Company cards are only available on the Control plan, starting at ',
+ },
+ rules: {
+ title: 'Rules',
+ description: `Rules run in the background and keep your spend under control so you don't have to sweat the small stuff.\n\nRequire expense details like receipts and descriptions, set limits and defaults, and automate approvals and payments – all in one place.`,
+ onlyAvailableOnPlan: 'Rules are only available on the Control plan, starting at ',
+ },
pricing: {
amount: '$9 ',
perActiveMember: 'per active member per month.',
@@ -3574,6 +3608,16 @@ export default {
chatInAdmins: 'Chat in #admins',
addPaymentCard: 'Add payment card',
},
+ rules: {
+ individualExpenseRules: {
+ title: 'Expenses',
+ subtitle: 'Set spend controls and defaults for individual expenses. You can also create rules for',
+ },
+ expenseReportRules: {
+ title: 'Expense reports',
+ subtitle: 'Automate expense report compliance, approvals, and payment.',
+ },
+ },
},
getAssistancePage: {
title: 'Get assistance',
@@ -3719,6 +3763,11 @@ export default {
keyword: 'Keyword',
hasKeywords: 'Has keywords',
currency: 'Currency',
+ amount: {
+ lessThan: (amount?: string) => `Less than ${amount ?? ''}`,
+ greaterThan: (amount?: string) => `Greater than ${amount ?? ''}`,
+ between: (greaterThan: string, lessThan: string) => `Between ${greaterThan} and ${lessThan}`,
+ },
},
expenseType: 'Expense type',
},
@@ -3852,6 +3901,7 @@ export default {
stripePaid: ({amount, currency}: StripePaidParams) => `paid ${currency}${amount}`,
takeControl: `took control`,
unapproved: ({amount, currency}: UnapprovedParams) => `unapproved ${currency}${amount}`,
+ integrationSyncFailed: (label: string, errorMessage: string) => `failed to sync with ${label} ("${errorMessage}")`,
addEmployee: (email: string, role: string) => `added ${email} as ${role === 'user' ? 'member' : 'admin'}`,
updateRole: (email: string, currentRole: string, newRole: string) => `updated the role of ${email} from ${currentRole} to ${newRole}`,
removeMember: (email: string, role: string) => `removed ${role} ${email}`,
@@ -4173,6 +4223,7 @@ export default {
expand: 'Expand',
mute: 'Mute',
unmute: 'Unmute',
+ normal: 'Normal',
},
exitSurvey: {
header: 'Before you go',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 08ca657435b4..679a6297ffdd 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -743,11 +743,17 @@ export default {
yourCompanyWebsiteNote: 'Si no tiene un sitio web, puede proporcionar el perfil de LinkedIn o de las redes sociales de su empresa.',
invalidDomainError: 'Ha introducido un dominio no válido. Para continuar, introduzca un dominio válido.',
publicDomainError: 'Ha introducido un dominio público. Para continuar, introduzca un dominio privado.',
- expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) =>
- `${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${
- pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : ''
- }`,
-
+ expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => {
+ const expenseText = `${count} ${Str.pluralize('gasto', 'gastos', count)}`;
+ const statusText = [];
+ if (scanningReceipts > 0) {
+ statusText.push(`${scanningReceipts} escaneando`);
+ }
+ if (pendingReceipts > 0) {
+ statusText.push(`${pendingReceipts} pendiente`);
+ }
+ return statusText.length > 0 ? `${expenseText} (${statusText.join(', ')})` : expenseText;
+ },
deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Eliminar ${Str.pluralize('gasto', 'gastos', count)}`,
deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta solicitud', 'estas solicitudes', count)}?`,
settledExpensify: 'Pagado',
@@ -1436,7 +1442,7 @@ export default {
addPaymentMethod: 'Añadir método de pago',
addNewDebitCard: 'Añadir nueva tarjeta de débito',
addNewBankAccount: 'Añadir nueva cuenta de banco',
- accountLastFour: 'Cuenta terminada en',
+ accountLastFour: 'Terminada en',
cardLastFour: 'Tarjeta terminada en',
addFirstPaymentMethod: 'Añade un método de pago para enviar y recibir pagos directamente desde la aplicación.',
defaultPaymentMethod: 'Predeterminado',
@@ -2137,6 +2143,7 @@ export default {
expensifyCard: 'Tarjeta Expensify',
workflows: 'Flujos de trabajo',
workspace: 'Espacio de trabajo',
+ companyCards: 'Tarjetas de empresa',
edit: 'Editar espacio de trabajo',
enabled: 'Activada',
disabled: 'Desactivada',
@@ -2153,6 +2160,7 @@ export default {
travel: 'Viajes',
members: 'Miembros',
accounting: 'Contabilidad',
+ rules: 'Reglas',
plan: 'Plan',
profile: 'Perfil',
bankAccount: 'Cuenta bancaria',
@@ -2631,7 +2639,7 @@ export default {
},
},
customersOrJobs: {
- title: 'Clientes / proyectos',
+ title: 'Clientes/proyectos',
subtitle: 'Elija cómo manejar los *clientes* y *proyectos* de NetSuite en Expensify.',
importCustomers: 'Importar clientes',
importJobs: 'Importar proyectos',
@@ -2646,7 +2654,7 @@ export default {
customSegments: {
title: 'Segmentos/registros personalizados',
addText: 'Añadir segmento/registro personalizado',
- recordTitle: 'Segmento personalizado',
+ recordTitle: 'Segmento/registro personalizado',
helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS,
helpLinkText: 'Ver instrucciones detalladas',
helpText: ' sobre la configuración de segmentos/registros personalizado.',
@@ -2880,6 +2888,10 @@ export default {
title: 'Gasto',
subtitle: 'Habilita otras funcionalidades que ayudan a aumentar tu equipo.',
},
+ manageSection: {
+ title: 'Gestionar',
+ subtitle: 'Añade controles que ayudan a mantener los gastos dentro del presupuesto.',
+ },
earnSection: {
title: 'Gane',
subtitle: 'Habilita funciones opcionales para agilizar tus ingresos y recibir pagos más rápido.',
@@ -2909,6 +2921,13 @@ export default {
ctaTitle: 'Emitir nueva tarjeta',
},
},
+ companyCards: {
+ title: 'Tarjetas de empresa',
+ subtitle: 'Importar gastos de las tarjetas de empresa existentes.',
+ disableCardTitle: 'Deshabilitar tarjetas de empresa',
+ disableCardPrompt: 'No puedes deshabilitar las tarjetas de empresa porque esta función está en uso. Por favor, contacta a Concierge para los próximos pasos.',
+ disableCardButton: 'Chatear con Concierge',
+ },
distanceRates: {
title: 'Tasas de distancia',
subtitle: 'Añade, actualiza y haz cumplir las tasas.',
@@ -2947,6 +2966,10 @@ export default {
disconnectText: 'Para desactivar la contabilidad, desconecta tu conexión contable del espacio de trabajo.',
manageSettings: 'Gestionar la configuración',
},
+ rules: {
+ title: 'Reglas',
+ subtitle: 'Configura cuándo se exigen los recibos, marca los gastos elevados y mucho más.',
+ },
},
reportFields: {
addField: 'Añadir campo',
@@ -3593,6 +3616,16 @@ export default {
description: `Añada código de impuesto mayor a sus categorías para exportar fácilmente los gastos a sus sistemas de contabilidad y nómina.`,
onlyAvailableOnPlan: 'Los código de impuesto mayor solo están disponibles en el plan Control, a partir de ',
},
+ companyCards: {
+ title: 'Tarjetas de empresa',
+ description: `Las tarjetas de empresa le permiten importar los gastos de las tarjetas de empresa existentes de todos los principales emisores de tarjetas. Puede asignar tarjetas a empleados e importar transacciones automáticamente.`,
+ onlyAvailableOnPlan: 'Las tarjetas de empresa solo están disponibles en el plan Control, a partir de ',
+ },
+ rules: {
+ title: 'Reglas',
+ description: `Las reglas se ejecutan en segundo plano y mantienen tus gastos bajo control para que no tengas que preocuparte por los detalles pequeños.\n\nExige detalles de los gastos, como recibos y descripciones, establece límites y valores predeterminados, y automatiza las aprobaciones y los pagos, todo en un mismo lugar.`,
+ onlyAvailableOnPlan: 'Las reglas están disponibles solo en el plan Control, que comienza en ',
+ },
note: {
upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o',
learnMore: 'más información',
@@ -3624,6 +3657,16 @@ export default {
chatInAdmins: 'Chatea en #admins',
addPaymentCard: 'Agregar tarjeta de pago',
},
+ rules: {
+ individualExpenseRules: {
+ title: 'Gastos',
+ subtitle: 'Establece controles y valores predeterminados para gastos individuales. También puedes crear reglas para',
+ },
+ expenseReportRules: {
+ title: 'Informes de gastos',
+ subtitle: 'Automatiza el cumplimiento, la aprobación y el pago de los informes de gastos.',
+ },
+ },
},
getAssistancePage: {
title: 'Obtener ayuda',
@@ -3770,6 +3813,11 @@ export default {
keyword: 'Palabra clave',
hasKeywords: 'Tiene palabras clave',
currency: 'Divisa',
+ amount: {
+ lessThan: (amount?: string) => `Menos de ${amount ?? ''}`,
+ greaterThan: (amount?: string) => `Más que ${amount ?? ''}`,
+ between: (greaterThan: string, lessThan: string) => `Entre ${greaterThan} y ${lessThan}`,
+ },
},
expenseType: 'Tipo de gasto',
},
@@ -3904,6 +3952,7 @@ export default {
stripePaid: ({amount, currency}: StripePaidParams) => `pagado ${currency}${amount}`,
takeControl: `tomó el control`,
unapproved: ({amount, currency}: UnapprovedParams) => `no aprobado ${currency}${amount}`,
+ integrationSyncFailed: (label: string, errorMessage: string) => `no se pudo sincronizar con ${label} ("${errorMessage}")`,
addEmployee: (email: string, role: string) => `agregó a ${email} como ${role === 'user' ? 'miembro' : 'administrador'}`,
updateRole: (email: string, currentRole: string, newRole: string) =>
`actualicé el rol ${email} de ${currentRole === 'user' ? 'miembro' : 'administrador'} a ${newRole === 'user' ? 'miembro' : 'administrador'}`,
@@ -4688,6 +4737,7 @@ export default {
expand: 'Expandir',
mute: 'Silenciar',
unmute: 'Activar sonido',
+ normal: 'Normal',
},
exitSurvey: {
header: 'Antes de irte',
diff --git a/src/libs/API/parameters/EnablePolicyCompanyCardsParams.ts b/src/libs/API/parameters/EnablePolicyCompanyCardsParams.ts
new file mode 100644
index 000000000000..0bf3ce34b9d2
--- /dev/null
+++ b/src/libs/API/parameters/EnablePolicyCompanyCardsParams.ts
@@ -0,0 +1,7 @@
+type EnablePolicyCompanyCardsParams = {
+ authToken?: string | null;
+ policyID: string;
+ enabled: boolean;
+};
+
+export default EnablePolicyCompanyCardsParams;
diff --git a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts
index 2659fac6810a..057b6188e3ea 100644
--- a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts
+++ b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts
@@ -1,7 +1,8 @@
-import type {SearchStatus} from '@components/Search/types';
+import type {SearchQueryString, SearchStatus} from '@components/Search/types';
type ExportSearchItemsToCSVParams = {
query: SearchStatus;
+ jsonQuery: SearchQueryString;
reportIDList: string[];
transactionIDList: string[];
policyIDs: string[];
diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts
index c95ffce14b2c..e2cac84e0d12 100644
--- a/src/libs/API/parameters/SendInvoiceParams.ts
+++ b/src/libs/API/parameters/SendInvoiceParams.ts
@@ -20,6 +20,9 @@ type SendInvoiceParams = RequireAtLeastOne<
transactionThreadReportID: string;
companyName?: string;
companyWebsite?: string;
+ createdIOUReportActionID: string;
+ createdReportActionIDForThread: string;
+ reportActionID: string;
},
'receiverEmail' | 'receiverInvoiceRoomID'
>;
diff --git a/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts b/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts
new file mode 100644
index 000000000000..c748a98e4119
--- /dev/null
+++ b/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts
@@ -0,0 +1,6 @@
+type SetPolicyRulesEnabledParams = {
+ policyID: string;
+ enabled: boolean;
+};
+
+export default SetPolicyRulesEnabledParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index c95355dada31..9696f4213a48 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -272,9 +272,11 @@ export type {default as ExportSearchItemsToCSVParams} from './ExportSearchItemsT
export type {default as UpdateExpensifyCardLimitParams} from './UpdateExpensifyCardLimitParams';
export type {CreateWorkspaceApprovalParams, UpdateWorkspaceApprovalParams, RemoveWorkspaceApprovalParams} from './WorkspaceApprovalParams';
export type {default as StartIssueNewCardFlowParams} from './StartIssueNewCardFlowParams';
+export type {default as SetPolicyRulesEnabledParams} from './SetPolicyRulesEnabledParams';
export type {default as ConfigureExpensifyCardsForPolicyParams} from './ConfigureExpensifyCardsForPolicyParams';
export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardParams';
export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams';
export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams';
+export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyCompanyCardsParams';
export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams';
export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index cc3504ad89ab..5ea2ae44b74d 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -199,7 +199,9 @@ const WRITE_COMMANDS = {
ENABLE_POLICY_WORKFLOWS: 'EnablePolicyWorkflows',
ENABLE_POLICY_REPORT_FIELDS: 'EnablePolicyReportFields',
ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards',
+ ENABLE_POLICY_COMPANY_CARDS: 'EnablePolicyCompanyCards',
ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing',
+ SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled',
SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax',
SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax',
SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName',
@@ -529,7 +531,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS]: Parameters.EnablePolicyWorkflowsParams;
[WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS]: Parameters.EnablePolicyReportFieldsParams;
[WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams;
+ [WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS]: Parameters.EnablePolicyCompanyCardsParams;
[WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams;
+ [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams;
[WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
[WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams;
[WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams;
diff --git a/src/libs/Browser/index.website.ts b/src/libs/Browser/index.website.ts
index b89190dc7f78..2007f2c6cbc0 100644
--- a/src/libs/Browser/index.website.ts
+++ b/src/libs/Browser/index.website.ts
@@ -79,12 +79,12 @@ const isSafari: IsSafari = () => getBrowser() === 'safari' || isMobileSafari();
/**
* The session information needs to be passed to the Desktop app, and the only way to do that is by using query params. There is no other way to transfer the data.
*/
-const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', email = '') => {
+const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', email = '', initialRoute = '') => {
const params = new URLSearchParams();
// If the user is opening the desktop app through a third party signin flow, we need to manually add the exitTo param
// so that the desktop app redirects to the correct home route after signin is complete.
const openingFromDesktopRedirect = window.location.pathname === `/${ROUTES.DESKTOP_SIGN_IN_REDIRECT}`;
- params.set('exitTo', `${openingFromDesktopRedirect ? '/r' : window.location.pathname}${window.location.search}${window.location.hash}`);
+ params.set('exitTo', `${openingFromDesktopRedirect ? '/r' : initialRoute || window.location.pathname}${window.location.search}${window.location.hash}`);
if (email && shortLivedAuthToken) {
params.set('email', email);
params.set('shortLivedAuthToken', shortLivedAuthToken);
diff --git a/src/libs/Browser/types.ts b/src/libs/Browser/types.ts
index cb242d3729aa..ff0de91e7b78 100644
--- a/src/libs/Browser/types.ts
+++ b/src/libs/Browser/types.ts
@@ -12,6 +12,6 @@ type IsChromeIOS = () => boolean;
type IsSafari = () => boolean;
-type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string) => void;
+type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string, initialRoute?: string) => void;
export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, IsChromeIOS, OpenRouteInDesktopApp};
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index fa98cf32ee39..2b2aad59d58d 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -248,7 +248,7 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number {
/**
* Returns custom unit rate ID for the distance transaction
*/
-function getCustomUnitRateID(reportID: string) {
+function getCustomUnitRateID(reportID: string, shouldUseDefault?: boolean) {
const allReports = ReportConnection.getAllReports();
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
@@ -259,7 +259,7 @@ function getCustomUnitRateID(reportID: string) {
const distanceUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
const lastSelectedDistanceRateID = lastSelectedDistanceRates?.[policy?.id ?? '-1'] ?? '-1';
const lastSelectedDistanceRate = distanceUnit?.rates[lastSelectedDistanceRateID] ?? {};
- if (lastSelectedDistanceRate.enabled && lastSelectedDistanceRateID) {
+ if (lastSelectedDistanceRate.enabled && lastSelectedDistanceRateID && !shouldUseDefault) {
customUnitRateID = lastSelectedDistanceRateID;
} else {
customUnitRateID = getDefaultMileageRate(policy)?.customUnitRateID ?? '-1';
diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts
index 9624d7ab992b..efe1c380dfd0 100644
--- a/src/libs/E2E/tests/reportTypingTest.e2e.ts
+++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts
@@ -1,13 +1,16 @@
import type {NativeConfig} from 'react-native-config';
import Config from 'react-native-config';
+import {runOnUI} from 'react-native-reanimated';
import E2ELogin from '@libs/E2E/actions/e2eLogin';
import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard';
import E2EClient from '@libs/E2E/client';
import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow';
+import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve';
import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import {getRerenderCount, resetRerenderCount} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e';
+import {onSubmitAction} from '@pages/home/report/ReportActionCompose/ReportActionCompose';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import * as NativeCommands from '../../../../tests/e2e/nativeCommands/NativeCommandsAction';
@@ -17,6 +20,7 @@ const test = (config: NativeConfig) => {
console.debug('[E2E] Logging in for typing');
const reportID = getConfigValueOrThrow('reportID', config);
+ const message = getConfigValueOrThrow('message', config);
E2ELogin().then((neededLogin) => {
if (neededLogin) {
@@ -28,7 +32,26 @@ const test = (config: NativeConfig) => {
console.debug('[E2E] Logged in, getting typing metrics and submitting them…');
+ const [renderTimesPromise, renderTimesResolve] = getPromiseWithResolve();
+ const [messageSentPromise, messageSentResolve] = getPromiseWithResolve();
+
+ Promise.all([renderTimesPromise, messageSentPromise]).then(() => {
+ console.debug(`[E2E] Submitting!`);
+
+ E2EClient.submitTestDone();
+ });
+
Performance.subscribeToMeasurements((entry) => {
+ if (entry.name === CONST.TIMING.MESSAGE_SENT) {
+ E2EClient.submitTestResults({
+ branch: Config.E2E_BRANCH,
+ name: 'Message sent',
+ metric: entry.duration,
+ unit: 'ms',
+ }).then(messageSentResolve);
+ return;
+ }
+
if (entry.name !== CONST.TIMING.SIDEBAR_LOADED) {
return;
}
@@ -46,18 +69,26 @@ const test = (config: NativeConfig) => {
return Promise.resolve();
})
.then(() => E2EClient.sendNativeCommand(NativeCommands.makeTypeTextCommand('A')))
- .then(() => {
- setTimeout(() => {
- const rerenderCount = getRerenderCount();
+ .then(
+ () =>
+ new Promise((resolve) => {
+ setTimeout(() => {
+ const rerenderCount = getRerenderCount();
- E2EClient.submitTestResults({
- branch: Config.E2E_BRANCH,
- name: 'Composer typing rerender count',
- metric: rerenderCount,
- unit: 'renders',
- }).then(E2EClient.submitTestDone);
- }, 3000);
- })
+ E2EClient.submitTestResults({
+ branch: Config.E2E_BRANCH,
+ name: 'Composer typing rerender count',
+ metric: rerenderCount,
+ unit: 'renders',
+ })
+ .then(renderTimesResolve)
+ .then(resolve);
+ }, 3000);
+ }),
+ )
+ .then(() => E2EClient.sendNativeCommand(NativeCommands.makeBackspaceCommand()))
+ .then(() => E2EClient.sendNativeCommand(NativeCommands.makeTypeTextCommand(message)))
+ .then(() => runOnUI(onSubmitAction)())
.catch((error) => {
console.error('[E2E] Error while test', error);
E2EClient.submitTestDone();
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index 986fd165d2d7..d1bf8fcd8c8c 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -141,10 +141,11 @@ function insertTagIntoTransactionTagsString(transactionTags: string, tag: string
const tagArray = TransactionUtils.getTagArrayFromName(transactionTags);
tagArray[tagIndex] = tag;
- return tagArray
- .map((tagItem) => tagItem.trim())
- .filter((tagItem) => !!tagItem)
- .join(CONST.COLON);
+ while (tagArray.length > 0 && !tagArray[tagArray.length - 1]) {
+ tagArray.pop();
+ }
+
+ return tagArray.map((tagItem) => tagItem.trim()).join(CONST.COLON);
}
function isMovingTransactionFromTrackExpense(action?: IOUAction) {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index f105de52a5ac..7722696245fd 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -514,17 +514,18 @@ const SearchReportModalStackNavigator = createModalStackNavigator({
[SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require('../../../../pages/Search/SearchFiltersDatePage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP]: () => require('../../../../pages/Search/SearchFiltersCurrencyPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: () => require('../../../../pages/Search/SearchFiltersDescriptionPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: () => require('../../../../pages/Search/SearchFiltersMerchantPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: () => require('../../../../pages/Search/SearchFiltersReportIDPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: () => require('../../../../pages/Search/SearchFiltersCategoryPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: () => require('../../../../pages/Search/SearchFiltersKeywordPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: () => require('../../../../pages/Search/SearchFiltersCardPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP]: () => require('../../../../pages/Search/SearchFiltersTaxRatePage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_EXPENSE_TYPE_RHP]: () => require('../../../../pages/Search/SearchFiltersExpenseTypePage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_TAG_RHP]: () => require('../../../../pages/Search/SearchFiltersTagPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersDatePage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersCurrencyPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersDescriptionPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersReportIDPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersCategoryPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersKeywordPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersTaxRatePage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_EXPENSE_TYPE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersExpenseTypePage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_TAG_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersTagPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersFromPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersToPage').default,
});
diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
index 748d92b49a1c..22a190913ed2 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
@@ -32,7 +32,9 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = {
[SCREENS.WORKSPACE.TAXES]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default,
[SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsPage').default,
[SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default,
+ [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default,
} satisfies Screens;
function FullScreenNavigator() {
diff --git a/src/libs/Navigation/isSearchTopmostCentralPane.ts b/src/libs/Navigation/isSearchTopmostCentralPane.ts
new file mode 100644
index 000000000000..3c315116bb00
--- /dev/null
+++ b/src/libs/Navigation/isSearchTopmostCentralPane.ts
@@ -0,0 +1,22 @@
+import SCREENS from '@src/SCREENS';
+import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute';
+import {navigationRef} from './Navigation';
+import type {RootStackParamList, State} from './types';
+
+const isSearchTopmostCentralPane = (): boolean => {
+ const rootState = navigationRef.getRootState() as State;
+
+ if (!rootState) {
+ return false;
+ }
+
+ const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState);
+
+ if (topmostCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) {
+ return true;
+ }
+
+ return false;
+};
+
+export default isSearchTopmostCentralPane;
diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index add67e5b273b..8a4a53e0f705 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -50,6 +50,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> =
SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP,
+ SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP,
SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP,
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 942a23068979..e4072ea1e696 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -160,6 +160,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
[SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE],
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: [],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [
SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW,
SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 236b56882dde..4d3f19984b8f 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1029,6 +1029,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_MERCHANT,
[SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_DESCRIPTION,
[SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_REPORT_ID,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_AMOUNT,
[SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CATEGORY,
[SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_KEYWORD,
[SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_CARD,
@@ -1059,6 +1060,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD.route,
},
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: {
+ path: ROUTES.WORKSPACE_COMPANY_CARDS.route,
+ },
[SCREENS.WORKSPACE.WORKFLOWS]: {
path: ROUTES.WORKSPACE_WORKFLOWS.route,
},
@@ -1098,6 +1102,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.DISTANCE_RATES]: {
path: ROUTES.WORKSPACE_DISTANCE_RATES.route,
},
+ [SCREENS.WORKSPACE.RULES]: {
+ path: ROUTES.WORKSPACE_RULES.route,
+ },
},
},
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index b689f36d8a35..ceb62f1dac1c 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1119,6 +1119,9 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.WORKFLOWS]: {
policyID: string;
};
@@ -1194,6 +1197,9 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.RULES]: {
+ policyID: string;
+ };
};
type OnboardingModalNavigatorParamList = {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 98274e829001..3f519f4de7c4 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -53,7 +53,6 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PhoneNumber from './PhoneNumber';
import * as PolicyUtils from './PolicyUtils';
import * as ReportActionUtils from './ReportActionsUtils';
-import * as ReportConnection from './ReportConnection';
import * as ReportUtils from './ReportUtils';
import * as TaskUtils from './TaskUtils';
import * as TransactionUtils from './TransactionUtils';
@@ -220,7 +219,6 @@ type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: bo
type FilterOptionsConfig = Pick & {
preferChatroomsOverThreads?: boolean;
- includeChatRoomsByParticipants?: boolean;
preferPolicyExpenseChat?: boolean;
};
@@ -351,35 +349,12 @@ Onyx.connect({
}, {});
},
});
-
-let allReportsDraft: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
- waitForCollectionCallback: true,
- callback: (value) => (allReportsDraft = value),
-});
-
let activePolicyID: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
callback: (value) => (activePolicyID = value),
});
-/**
- * Get the report or draft report given a reportID
- */
-function getReportOrDraftReport(reportID: string | undefined): OnyxEntry {
- const allReports = ReportConnection.getAllReports();
- if (!allReports && !allReportsDraft) {
- return undefined;
- }
-
- const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
- const draftReport = allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
-
- return report ?? draftReport;
-}
-
/**
* @param defaultValues {login: accountID} In workspace invite page, when new user is added we pass available data to opt in
* @returns Returns avatar data for a list of user accountIDs
@@ -473,31 +448,6 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant
};
}
-/**
- * Constructs a Set with all possible names (displayName, firstName, lastName, email) for all participants in a report,
- * to be used in isSearchStringMatch.
- */
-function getParticipantNames(personalDetailList?: Array> | null): Set {
- // We use a Set because `Set.has(value)` on a Set of with n entries is up to n (or log(n)) times faster than
- // `_.contains(Array, value)` for an Array with n members.
- const participantNames = new Set();
- personalDetailList?.forEach((participant) => {
- if (participant.login) {
- participantNames.add(participant.login.toLowerCase());
- }
- if (participant.firstName) {
- participantNames.add(participant.firstName.toLowerCase());
- }
- if (participant.lastName) {
- participantNames.add(participant.lastName.toLowerCase());
- }
- if (participant.displayName) {
- participantNames.add(PersonalDetailsUtils.getDisplayNameOrDefault(participant).toLowerCase());
- }
- });
- return participantNames;
-}
-
/**
* A very optimized method to remove duplicates from an array.
* Taken from https://stackoverflow.com/a/9229821/9114791
@@ -578,7 +528,7 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu
* Update alternate text for the option when applicable
*/
function getAlternateText(option: ReportUtils.OptionData, {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig) {
- const report = getReportOrDraftReport(option.reportID);
+ const report = ReportUtils.getReportOrDraftReport(option.reportID);
const isAdminRoom = ReportUtils.isAdminRoom(report);
const isAnnounceRoom = ReportUtils.isAnnounceRoom(report);
@@ -634,7 +584,7 @@ function getIOUReportIDOfLastAction(report: OnyxEntry): string | undefin
if (!ReportActionUtils.isReportPreviewAction(lastAction)) {
return;
}
- return getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastAction))?.reportID;
+ return ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastAction))?.reportID;
}
/**
@@ -675,7 +625,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
const properSchemaForMoneyRequestMessage = ReportUtils.getReportPreviewMessage(report, lastReportAction, true, false, null, true);
lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForMoneyRequestMessage);
} else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) {
- const iouReport = getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction));
+ const iouReport = ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction));
const lastIOUMoneyReportAction = allSortedReportActions[iouReport?.reportID ?? '-1']?.find(
(reportAction, key): reportAction is ReportAction =>
ReportActionUtils.shouldReportActionBeVisible(reportAction, key) &&
@@ -711,11 +661,11 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
} else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) {
lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction);
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
- lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(reportID);
+ lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(lastReportAction);
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
- lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(reportID);
+ lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(lastReportAction);
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) {
- lastMessageTextFromReport = ReportUtils.getIOUForwardedMessage(reportID);
+ lastMessageTextFromReport = ReportUtils.getIOUForwardedMessage(lastReportAction);
} else if (ReportActionUtils.isActionableAddPaymentCard(lastReportAction)) {
lastMessageTextFromReport = ReportActionUtils.getReportActionMessageText(lastReportAction);
} else if (lastReportAction?.actionName === 'EXPORTINTEGRATION') {
@@ -862,7 +812,7 @@ function createOption(
* Get the option for a given report.
*/
function getReportOption(participant: Participant): ReportUtils.OptionData {
- const report = getReportOrDraftReport(participant.reportID);
+ const report = ReportUtils.getReportOrDraftReport(participant.reportID);
const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true);
const option = createOption(
@@ -896,7 +846,7 @@ function getReportOption(participant: Participant): ReportUtils.OptionData {
* Get the option for a policy expense report.
*/
function getPolicyExpenseReportOption(participant: Participant | ReportUtils.OptionData): ReportUtils.OptionData {
- const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? getReportOrDraftReport(participant.reportID) : null;
+ const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? ReportUtils.getReportOrDraftReport(participant.reportID) : null;
const visibleParticipantAccountIDs = Object.entries(expenseReport?.participants ?? {})
.filter(([, reportParticipant]) => reportParticipant && !reportParticipant.hidden)
@@ -2438,7 +2388,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
maxRecentReportsToShow = 0,
excludeLogins = [],
preferChatroomsOverThreads = false,
- includeChatRoomsByParticipants = false,
preferPolicyExpenseChat = false,
} = config ?? {};
if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) {
@@ -2455,29 +2404,9 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
optionsToExclude.push({login});
});
- const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => {
- const keys: string[] = [];
- const visibleChatMemberAccountIDs = item.participantsList ?? [];
- if (allPersonalDetails) {
- visibleChatMemberAccountIDs.forEach((participant) => {
- const login = participant?.login;
-
- if (participant?.displayName) {
- keys.push(participant.displayName);
- }
-
- if (login) {
- keys.push(login);
- keys.push(login.replace(CONST.EMAIL_SEARCH_REGEX, ''));
- }
- });
- }
-
- return keys;
- };
const matchResults = searchTerms.reduceRight((items, term) => {
const recentReports = filterArrayByMatch(items.recentReports, term, (item) => {
- let values: string[] = [];
+ const values: string[] = [];
if (item.text) {
values.push(item.text);
}
@@ -2491,21 +2420,10 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
if (item.alternateText) {
values.push(item.alternateText);
}
- values = values.concat(getParticipantsLoginsArray(item));
} else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) {
if (item.subtitle) {
values.push(item.subtitle);
}
-
- if (includeChatRoomsByParticipants) {
- values = values.concat(getParticipantsLoginsArray(item));
- }
- }
-
- if (!item.isChatRoom) {
- const participantNames = getParticipantNames(item.participantsList ?? []);
- values = values.concat(Array.from(participantNames));
- values = values.concat(getParticipantsLoginsArray(item));
}
return uniqFast(values);
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index 15c15e113c8c..0a6756034f7d 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -36,6 +36,10 @@ function canUseNetSuiteUSATax(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.NETSUITE_USA_TAX) || canUseAllBetas(betas);
}
+function canUseWorkspaceRules(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.WORKSPACE_RULES) || canUseAllBetas(betas);
+}
+
/**
* Link previews are temporarily disabled.
*/
@@ -52,4 +56,5 @@ export default {
canUseSpotnanaTravel,
canUseWorkspaceFeeds,
canUseNetSuiteUSATax,
+ canUseWorkspaceRules,
};
diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts
index 6f87e09a61d7..fe3ea7de3bac 100644
--- a/src/libs/PersonalDetailsUtils.ts
+++ b/src/libs/PersonalDetailsUtils.ts
@@ -33,8 +33,19 @@ Onyx.connect({
},
});
-const hiddenTranslation = Localize.translateLocal('common.hidden');
-const youTranslation = Localize.translateLocal('common.you').toLowerCase();
+let hiddenTranslation = '';
+let youTranslation = '';
+
+Onyx.connect({
+ key: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ callback: (value) => {
+ if (!value) {
+ return;
+ }
+ hiddenTranslation = Localize.translateLocal('common.hidden');
+ youTranslation = Localize.translateLocal('common.you').toLowerCase();
+ },
+});
const regexMergedAccount = new RegExp(CONST.REGEX.MERGED_ACCOUNT_PREFIX);
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 54fd137c7c86..4d126cf9cbf4 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -152,6 +152,13 @@ function isReportPreviewAction(reportAction: OnyxInputOrEntry): re
function isSubmittedAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED);
}
+function isApprovedAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
+ return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.APPROVED);
+}
+
+function isForwardedAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
+ return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.FORWARDED);
+}
function isModifiedExpenseAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE);
@@ -1730,6 +1737,9 @@ export {
isTransactionThread,
isTripPreview,
isWhisperAction,
+ isSubmittedAction,
+ isApprovedAction,
+ isForwardedAction,
isWhisperActionTargetedToOthers,
shouldHideNewMarker,
shouldReportActionBeVisible,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 2c4d6d9bc1ed..38b73ffc2057 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -658,14 +658,7 @@ function getChatType(report: OnyxInputOrEntry | Participant): ValueOf {
const allReports = ReportConnection.getAllReports();
- if (!allReports && !allReportsDraft) {
- return undefined;
- }
-
- const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
- const draftReport = allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
-
- return report ?? draftReport;
+ return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
}
/**
@@ -3671,6 +3664,16 @@ function getReportName(
const parentReportAction = parentReportActionParam ?? ReportActionsUtils.getParentReportAction(report);
const parentReportActionMessage = ReportActionsUtils.getReportActionMessage(parentReportAction);
+ if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) {
+ return getIOUSubmittedMessage(parentReportAction);
+ }
+ if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.FORWARDED) {
+ return getIOUForwardedMessage(parentReportAction);
+ }
+ if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.APPROVED) {
+ return getIOUApprovedMessage(parentReportAction);
+ }
+
if (isChatThread(report)) {
if (!isEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) {
formattedName = getTransactionReportName(parentReportAction);
@@ -4373,23 +4376,25 @@ function buildOptimisticExpenseReport(
return expenseReport;
}
-function getFormattedAmount(reportID: string) {
- const report = getReportOrDraftReport(reportID);
- const linkedReport = isChatThread(report) ? getParentReport(report) : report;
- const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(linkedReport?.total ?? 0), linkedReport?.currency);
+function getFormattedAmount(reportAction: ReportAction) {
+ if (!ReportActionsUtils.isSubmittedAction(reportAction) && !ReportActionsUtils.isForwardedAction(reportAction) && !ReportActionsUtils.isApprovedAction(reportAction)) {
+ return '';
+ }
+ const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
+ const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(originalMessage?.amount ?? 0), originalMessage?.currency);
return formattedAmount;
}
-function getIOUSubmittedMessage(reportID: string) {
- return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportID)});
+function getIOUSubmittedMessage(reportAction: ReportAction) {
+ return Localize.translateLocal('iou.submittedAmount', {formattedAmount: getFormattedAmount(reportAction)});
}
-function getIOUApprovedMessage(reportID: string) {
- return Localize.translateLocal('iou.approvedAmount', {amount: getFormattedAmount(reportID)});
+function getIOUApprovedMessage(reportAction: ReportAction) {
+ return Localize.translateLocal('iou.approvedAmount', {amount: getFormattedAmount(reportAction)});
}
-function getIOUForwardedMessage(reportID: string) {
- return Localize.translateLocal('iou.forwardedAmount', {amount: getFormattedAmount(reportID)});
+function getIOUForwardedMessage(reportAction: ReportAction) {
+ return Localize.translateLocal('iou.forwardedAmount', {amount: getFormattedAmount(reportAction)});
}
function getWorkspaceNameUpdatedMessage(action: ReportAction) {
@@ -4431,7 +4436,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num
iouMessage = `approved ${amount}`;
break;
case CONST.REPORT.ACTIONS.TYPE.FORWARDED:
- iouMessage = getIOUForwardedMessage(iouReportID);
+ iouMessage = Localize.translateLocal('iou.forwardedAmount', {amount});
break;
case CONST.REPORT.ACTIONS.TYPE.UNAPPROVED:
iouMessage = `unapproved ${amount}`;
@@ -5808,7 +5813,11 @@ function doesTransactionThreadHaveViolations(
if (report?.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report?.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) {
return false;
}
- return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations) || TransactionUtils.hasWarningTypeViolation(IOUTransactionID, transactionViolations);
+ return (
+ TransactionUtils.hasViolation(IOUTransactionID, transactionViolations) ||
+ TransactionUtils.hasWarningTypeViolation(IOUTransactionID, transactionViolations) ||
+ TransactionUtils.hasModifiedAmountOrDateViolation(IOUTransactionID, transactionViolations)
+ );
}
/**
@@ -7831,6 +7840,7 @@ export {
getReportParticipantsTitle,
getReportPreviewMessage,
getReportRecipientAccountIDs,
+ getReportOrDraftReport,
getRoom,
getRootParentReport,
getRouteFromLink,
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index bb49697a8345..ce2429c5653b 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -441,6 +441,28 @@ function buildDateFilterQuery(filterValues: Partial)
return dateFilter;
}
+/**
+ * @private
+ * returns Date filter query string part, which needs special logic
+ */
+function buildAmountFilterQuery(filterValues: Partial) {
+ const lessThan = filterValues[FILTER_KEYS.LESS_THAN];
+ const greaterThan = filterValues[FILTER_KEYS.GREATER_THAN];
+
+ let amountFilter = '';
+ if (greaterThan) {
+ amountFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT}>${greaterThan}`;
+ }
+ if (lessThan && greaterThan) {
+ amountFilter += ' ';
+ }
+ if (lessThan) {
+ amountFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.AMOUNT}<${lessThan}`;
+ }
+
+ return amountFilter;
+}
+
function sanitizeString(str: string) {
if (str.includes(' ') || str.includes(',')) {
return `"${str}"`;
@@ -464,42 +486,43 @@ function getExpenseTypeTranslationKey(expenseType: ValueOf) {
- const filtersString = Object.entries(filterValues)
- .map(([filterKey, filterValue]) => {
- if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID || filterKey === FILTER_KEYS.KEYWORD) && filterValue) {
- const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
- if (keyInCorrectForm) {
- return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValue as string}`;
- }
+ const filtersString = Object.entries(filterValues).map(([filterKey, filterValue]) => {
+ if ((filterKey === FILTER_KEYS.MERCHANT || filterKey === FILTER_KEYS.DESCRIPTION || filterKey === FILTER_KEYS.REPORT_ID || filterKey === FILTER_KEYS.KEYWORD) && filterValue) {
+ const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
+ if (keyInCorrectForm) {
+ return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValue as string}`;
}
+ }
- if (
- (filterKey === FILTER_KEYS.CATEGORY ||
- filterKey === FILTER_KEYS.CARD_ID ||
- filterKey === FILTER_KEYS.TAX_RATE ||
- filterKey === FILTER_KEYS.EXPENSE_TYPE ||
- filterKey === FILTER_KEYS.TAG ||
- filterKey === FILTER_KEYS.CURRENCY ||
- filterKey === FILTER_KEYS.FROM ||
- filterKey === FILTER_KEYS.TO) &&
- Array.isArray(filterValue) &&
- filterValue.length > 0
- ) {
- const filterValueArray = filterValues[filterKey] ?? [];
- const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
- if (keyInCorrectForm) {
- return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`;
- }
+ if (
+ (filterKey === FILTER_KEYS.CATEGORY ||
+ filterKey === FILTER_KEYS.CARD_ID ||
+ filterKey === FILTER_KEYS.TAX_RATE ||
+ filterKey === FILTER_KEYS.EXPENSE_TYPE ||
+ filterKey === FILTER_KEYS.TAG ||
+ filterKey === FILTER_KEYS.CURRENCY ||
+ filterKey === FILTER_KEYS.FROM ||
+ filterKey === FILTER_KEYS.TO) &&
+ Array.isArray(filterValue) &&
+ filterValue.length > 0
+ ) {
+ const filterValueArray = filterValues[filterKey] ?? [];
+ const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as KeysOfFilterKeysObject[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey);
+ if (keyInCorrectForm) {
+ return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`;
}
+ }
- return undefined;
- })
- .filter(Boolean)
- .join(' ');
+ return undefined;
+ });
const dateFilter = buildDateFilterQuery(filterValues);
+ filtersString.push(dateFilter);
+
+ const amountFilter = buildAmountFilterQuery(filterValues);
+ filtersString.push(amountFilter);
- return dateFilter ? `${filtersString} ${dateFilter}` : filtersString;
+ return filtersString.filter(Boolean).join(' ');
}
function getFilters(queryJSON: SearchQueryJSON) {
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 3aaf4476eb0b..e5bd5d9b0753 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -4,7 +4,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {TransactionMergeParams} from '@libs/API/parameters';
-import {isCorporateCard, isExpensifyCard} from '@libs/CardUtils';
import {getCurrencyDecimals} from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -507,18 +506,18 @@ function getFormattedCreated(transaction: OnyxInputOrEntry, dateFor
* Determine whether a transaction is made with an Expensify card.
*/
function isExpensifyCardTransaction(transaction: OnyxEntry): boolean {
- if (!transaction?.cardID) {
- return false;
- }
- return isExpensifyCard(transaction.cardID);
+ return transaction?.bank === CONST.EXPENSIFY_CARD.BANK;
}
/**
* Determine whether a transaction is made with a card (Expensify or Company Card).
*/
function isCardTransaction(transaction: OnyxEntry): boolean {
- const cardID = transaction?.cardID ?? -1;
- return isCorporateCard(cardID);
+ return !!transaction?.managedCard;
+}
+
+function getCardName(transaction: OnyxEntry): string {
+ return transaction?.cardName ?? '';
}
/**
@@ -734,6 +733,15 @@ function hasWarningTypeViolation(transactionID: string, transactionViolations: O
return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.WARNING);
}
+/**
+ * Checks if any violations for the provided transaction are of modifiedAmount or modifiedDate
+ */
+function hasModifiedAmountOrDateViolation(transactionID: string, transactionViolations: OnyxCollection): boolean {
+ return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some(
+ (violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.MODIFIED_AMOUNT || violation.name === CONST.VIOLATIONS.MODIFIED_DATE,
+ );
+}
+
/**
* Calculates tax amount from the given expense amount and tax percentage
*/
@@ -1107,6 +1115,7 @@ export {
hasViolation,
hasNoticeTypeViolation,
hasWarningTypeViolation,
+ hasModifiedAmountOrDateViolation,
isCustomUnitRateIDForP2P,
getRateID,
getTransaction,
@@ -1116,6 +1125,7 @@ export {
buildTransactionsMergeParams,
getReimbursable,
isPayAtEndExpense,
+ getCardName,
};
export type {TransactionChanges};
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 7be767d73f48..0c1a157f76f3 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -449,7 +449,7 @@ function redirectThirdPartyDesktopSignIn() {
/**
* @param shouldAuthenticateWithCurrentAccount Optional, indicates whether default authentication method (shortLivedAuthToken) should be used
*/
-function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true) {
+function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true, initialRoute?: string) {
// There's no support for anonymous users on desktop
if (Session.isAnonymousUser()) {
return;
@@ -475,7 +475,7 @@ function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true) {
return;
}
- Browser.openRouteInDesktopApp(response.shortLivedAuthToken, currentUserEmail);
+ Browser.openRouteInDesktopApp(response.shortLivedAuthToken, currentUserEmail, initialRoute);
});
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index e84a14836efd..90d8d7311b1c 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -37,6 +37,7 @@ import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as IOUUtils from '@libs/IOUUtils';
import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import * as Localize from '@libs/Localize';
+import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane';
import Navigation from '@libs/Navigation/Navigation';
import * as NextStepUtils from '@libs/NextStepUtils';
import {rand64} from '@libs/NumberUtils';
@@ -111,6 +112,9 @@ type SendInvoiceInformation = {
reportPreviewReportActionID: string;
transactionID: string;
transactionThreadReportID: string;
+ createdIOUReportActionID: string;
+ createdReportActionIDForThread: string;
+ reportActionID: string;
onyxData: OnyxData;
};
@@ -161,13 +165,6 @@ Onyx.connect({
},
});
-let allReportsDraft: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
- waitForCollectionCallback: true,
- callback: (value) => (allReportsDraft = value),
-});
-
let allTransactions: NonNullable> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION,
@@ -275,21 +272,6 @@ Onyx.connect({
callback: (value) => (activePolicyID = value),
});
-/**
- * Get the report or draft report given a reportID
- */
-function getReportOrDraftReport(reportID: string | undefined): OnyxEntry