• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / d4d9f278-d845-4992-8c81-4f3757c427a1

08 Sep 2025 02:07PM UTC coverage: 86.303% (-1.8%) from 88.121%
d4d9f278-d845-4992-8c81-4f3757c427a1

Pull #5842

circleci

joeherm
fix(deploy): Update CircleCI to use common Dockerfile for building frontend
Pull Request #5842: fix(deploy): Unify Dockerfiles

2744 of 3951 branches covered (69.45%)

Branch coverage included in aggregate %.

17910 of 19981 relevant lines covered (89.64%)

9.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.52
/frontend/src/components/dashboard/aliases/AliasList.tsx
1
import { useState, useEffect } from "react";
3✔
2
import styles from "./AliasList.module.scss";
3✔
3
import { AliasData, isRandomAlias } from "../../../hooks/api/aliases";
3✔
4
import { ProfileData } from "../../../hooks/api/profile";
5
import { Alias } from "./Alias";
3✔
6
import { filterAliases } from "../../../functions/filterAliases";
3✔
7
import { CategoryFilter, SelectedFilters } from "./CategoryFilter";
3✔
8
import { UserData } from "../../../hooks/api/user";
9
import { RuntimeData } from "../../../hooks/api/types";
10
import { useLocalLabels } from "../../../hooks/localLabels";
3✔
11
import { AliasGenerationButton } from "./AliasGenerationButton";
3✔
12
import { SearchIcon } from "../../Icons";
3✔
13
import { useFlaggedAnchorLinks } from "../../../hooks/flaggedAnchorLinks";
3✔
14
import { useL10n } from "../../../hooks/l10n";
3✔
15
import { Localized } from "../../Localized";
3✔
16
import { VisuallyHidden } from "../../VisuallyHidden";
3✔
17
import { MaskCard } from "./MaskCard";
3✔
18
import { isFlagActive } from "../../../functions/waffle";
3✔
19

20
export type Props = {
21
  aliases: AliasData[];
22
  profile: ProfileData;
23
  user: UserData;
24
  runtimeData?: RuntimeData;
25
  onCreate: (
26
    options:
27
      | { mask_type: "random" }
28
      | { mask_type: "custom"; address: string; blockPromotionals: boolean },
29
  ) => void;
30
  onUpdate: (alias: AliasData, updatedFields: Partial<AliasData>) => void;
31
  onDelete: (alias: AliasData) => void;
32
  onboarding?: boolean;
33
  children?: React.ReactNode;
34
  setModalOpenedState?: (state: boolean) => void;
35
};
36

37
/**
38
 * Display a list of <Alias> cards, with the ability to filter them or create a new alias.
39
 */
40
export const AliasList = (props: Props) => {
53✔
41
  const l10n = useL10n();
165✔
42
  const [stringFilterInput, setStringFilterInput] = useState("");
165✔
43
  const [stringFilterVisible, setStringFilterVisible] = useState(false);
165✔
44
  const [resetCheckBoxes, setCheckboxes] = useState(false);
165✔
45
  const [categoryFilters, setCategoryFilters] = useState<SelectedFilters>({});
165✔
46
  const [localLabels, storeLocalLabel] = useLocalLabels();
165✔
47
  const [generatedAlias, setGeneratedAlias] = useState<AliasData | undefined>(
165✔
48
    undefined,
49
  );
50
  const { onboarding = false } = props;
165✔
51
  // When <AliasList> gets added to the page, if there's an anchor link in the
52
  // URL pointing to a mask, scroll to that mask:
53
  useFlaggedAnchorLinks(
165✔
54
    [],
55
    props.aliases.map((alias) => encodeURIComponent(alias.full_address)),
207✔
56
  );
57
  const [openAlias, setOpenAlias] = useState<AliasData | undefined>(
165✔
58
    // If the mask was focused on by an anchor link, expand that one on page load:
59
    props.aliases.find(
60
      (alias) =>
61
        alias.full_address ===
207✔
62
        decodeURIComponent(document.location.hash.substring(1)),
63
    ),
64
  );
65
  const [existingAliases, setExistingAliases] = useState<AliasData[]>(
165✔
66
    props.aliases,
67
  );
68

69
  useEffect(() => {
165✔
70
    if (props.aliases.length === 0) {
52✔
71
      setOpenAlias(undefined);
1✔
72
    } else {
73
      const existingAliasIds = existingAliases.map((alias) => alias.id);
84✔
74
      const newAliases = props.aliases.filter(
51✔
75
        (alias) => existingAliasIds.indexOf(alias.id) === -1,
84✔
76
      );
77
      if (newAliases.length !== 0) {
51!
78
        setOpenAlias(newAliases[0]);
×
79
      }
80
    }
81
    setExistingAliases(props.aliases);
52✔
82
  }, [props.aliases, existingAliases]);
83

84
  if (props.aliases.length === 0) {
165✔
85
    return null;
1✔
86
  }
87

88
  const aliasesWithLocalLabels = props.aliases.map((alias) => {
164✔
89
    const aliasWithLocalLabel = { ...alias };
207✔
90
    if (
207✔
91
      alias.description.length === 0 &&
411✔
92
      props.profile.server_storage === false &&
93
      localLabels !== null
94
    ) {
95
      const localLabel = localLabels.find(
52✔
96
        (localLabel) =>
97
          localLabel.id === alias.id &&
51✔
98
          localLabel.mask_type === alias.mask_type,
99
      );
100
      if (localLabel !== undefined) {
52✔
101
        aliasWithLocalLabel.description = localLabel.description;
51✔
102
      }
103
    }
104
    return aliasWithLocalLabel;
207✔
105
  });
106

107
  const aliases = sortAliases(
164✔
108
    filterAliases(aliasesWithLocalLabels, {
109
      ...categoryFilters,
110
      string: stringFilterInput,
111
    }),
112
  );
113

114
  const findAliasDataFromPrefix = (
164✔
115
    aliasPrefix: string,
116
  ): AliasData | undefined => {
117
    return aliases.find((alias) => aliasPrefix === alias.address);
×
118
  };
119

120
  const aliasCards = aliases.map((alias) => {
164✔
121
    const onUpdate = (updatedFields: Partial<AliasData>) => {
149✔
122
      if (
4✔
123
        localLabels !== null &&
8✔
124
        typeof updatedFields.description === "string" &&
125
        props.profile.server_storage === false
126
      ) {
127
        storeLocalLabel(alias, updatedFields.description);
1✔
128
        delete updatedFields.description;
1✔
129
      }
130
      return props.onUpdate(alias, updatedFields);
4✔
131
    };
132

133
    const onChangeOpen = (isOpen: boolean) => {
149✔
134
      if (isOpen === true) {
×
135
        setOpenAlias(alias);
×
136
      } else if (openAlias !== undefined && openAlias.id === alias.id) {
×
137
        setOpenAlias(undefined);
×
138
      }
139
    };
140

141
    return (
149✔
142
      <li
143
        className={styles["alias-card-wrapper"]}
144
        key={alias.address + isRandomAlias(alias)}
145
        id={encodeURIComponent(alias.full_address)}
146
      >
147
        {isFlagActive(props.runtimeData, "mask_redesign") ? (
148
          <MaskCard
2✔
149
            mask={alias}
150
            user={props.user}
151
            profile={props.profile}
152
            onUpdate={onUpdate}
153
            onDelete={() => props.onDelete(alias)}
×
154
            isOpen={
155
              openAlias !== undefined &&
2!
156
              openAlias.id === alias.id &&
157
              openAlias.mask_type === alias.mask_type
158
            }
159
            onChangeOpen={onChangeOpen}
160
            showLabelEditor={
161
              props.profile.server_storage || localLabels !== null
2!
162
            }
163
            runtimeData={props.runtimeData}
164
            isOnboarding={onboarding}
165
            copyAfterMaskGeneration={generatedAlias?.id === alias.id}
166
            setModalOpenedState={props.setModalOpenedState}
167
          >
168
            {props.children}
169
          </MaskCard>
170
        ) : (
171
          <Alias
172
            alias={alias}
173
            user={props.user}
174
            profile={props.profile}
175
            onUpdate={onUpdate}
176
            onDelete={() => props.onDelete(alias)}
1✔
177
            isOpen={
178
              openAlias !== undefined &&
147!
179
              openAlias.id === alias.id &&
180
              openAlias.mask_type === alias.mask_type
181
            }
182
            onChangeOpen={onChangeOpen}
183
            showLabelEditor={
184
              props.profile.server_storage || localLabels !== null
180✔
185
            }
186
            runtimeData={props.runtimeData}
187
          />
188
        )}
189
      </li>
190
    );
191
  });
192

193
  // With at most five aliases, filters aren't really useful
194
  // for non-Premium users.
195
  const categoryFilter = props.profile.has_premium ? (
164✔
196
    <div className={styles["category-filter"]}>
197
      <CategoryFilter
198
        onChange={setCategoryFilters}
199
        selectedFilters={categoryFilters}
200
        resetChecks={resetCheckBoxes}
201
        setCheckboxes={setCheckboxes}
202
      />
203
    </div>
204
  ) : null;
205

206
  const emptyStateMessage =
207
    props.aliases.length > 0 && aliases.length === 0 ? (
164✔
208
      <Localized
209
        id="profile-filter-no-results"
210
        elems={{
211
          "clear-button": (
212
            <button
213
              onClick={() => {
214
                setCategoryFilters({});
×
215
                setCheckboxes(true);
×
216
                setStringFilterInput("");
×
217
              }}
218
              className={styles["clear-filters-button"]}
219
            />
220
          ),
221
        }}
222
      >
223
        <p className={styles["empty-state-message"]} />
224
      </Localized>
225
    ) : null;
226

227
  const handleOnChange = (value: string) => {
164✔
228
    // removes Unicode characters found within the range of 0080 to FFFF
229
    setStringFilterInput(value.replace(/[\u{0080}-\u{FFFF}]/gu, ""));
104✔
230
  };
231

232
  return (
233
    <section className={styles["alias-list-container"]}>
234
      {isFlagActive(props.runtimeData, "free_user_onboarding") &&
164!
235
      onboarding ? null : (
×
236
        <div className={styles.controls}>
237
          <div
238
            className={`${styles["string-filter"]} ${
239
              stringFilterVisible ? styles["is-visible"] : ""
164!
240
            }`}
241
          >
242
            <VisuallyHidden>
243
              <label htmlFor="stringFilter">
244
                {l10n.getString("profile-filter-search-placeholder-2")}
245
              </label>
246
            </VisuallyHidden>
247
            <input
248
              value={stringFilterInput}
249
              onChange={(e) => handleOnChange(e.target.value)}
104✔
250
              type="search"
251
              name="stringFilter"
252
              id="stringFilter"
253
              placeholder={l10n.getString(
254
                "profile-filter-search-placeholder-2",
255
              )}
256
            />
257
            <span className={styles["match-count"]}>
258
              {aliases.length}/{props.aliases.length}
259
            </span>
260
          </div>
261
          <button
262
            onClick={() => setStringFilterVisible(!stringFilterVisible)}
×
263
            title={l10n.getString("profile-filter-search-placeholder-2")}
264
            className={`${styles["string-filter-toggle"]} ${
265
              stringFilterVisible ? styles["active"] : ""
164!
266
            }`}
267
          >
268
            <SearchIcon
269
              alt={l10n.getString("profile-filter-search-placeholder-2")}
270
              width={20}
271
              height={20}
272
            />
273
          </button>
274
          {categoryFilter}
275
          <div className={styles["new-alias-button"]}>
276
            <AliasGenerationButton
277
              aliases={props.aliases}
278
              profile={props.profile}
279
              runtimeData={props.runtimeData}
280
              onCreate={props.onCreate}
281
              onUpdate={props.onUpdate}
282
              findAliasDataFromPrefix={findAliasDataFromPrefix}
283
              setGeneratedAlias={setGeneratedAlias}
284
            />
285
          </div>
286
        </div>
287
      )}
288
      <ul>
289
        {isFlagActive(props.runtimeData, "free_user_onboarding") && onboarding
328!
290
          ? aliasCards[0]
291
          : aliasCards}
292
      </ul>
293
      {emptyStateMessage}
294
    </section>
295
  );
296
};
297

298
function sortAliases(aliases: AliasData[]): AliasData[] {
299
  const aliasDataCopy = aliases.slice();
164✔
300
  aliasDataCopy.sort((aliasA, aliasB) => {
164✔
301
    // `Date.parse` can be inconsistent,
302
    // but should be fairly reliable in parsing ISO 8601 strings
303
    // (though if Temporal ever gets accepted by TC39, we should switch to that):
304
    const aliasATimestamp = Date.parse(aliasA.created_at);
39✔
305
    const aliasBTimestamp = Date.parse(aliasB.created_at);
39✔
306
    return aliasBTimestamp - aliasATimestamp;
39✔
307
  });
308
  return aliasDataCopy;
164✔
309
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc