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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions dashboard/src/v2/components/settings/BranchNameSchemeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FunctionComponent } from "preact";
import { TextInput } from "./SettingsFormFields.js";
import { BRANCH_NAME_TOKEN_LABELS, TASK_PR_TITLE_TOKEN_LABELS } from "../../lib/settings-view-models.js";
import { getBranchNameTokenLabels, getTaskPrTitleTokenLabels } from "../../lib/settings-view-models.js";
import { useSettingsOperationsTranslations } from "../../i18n/messages/settings-operations.js";

export interface BranchNameSchemeEditorProps {
value: string;
Expand All @@ -26,8 +27,9 @@ const TemplateSchemeEditor: FunctionComponent<TemplateSchemeEditorProps> = ({
ariaLabel,
ariaDescription,
tokenLabels,
}) => (
<div className="flex flex-col gap-2 min-w-0 w-full">
}) => {
const { t } = useSettingsOperationsTranslations();
return <div className="flex flex-col gap-2 min-w-0 w-full">
<TextInput
value={value || ""}
onChange={onChange}
Expand All @@ -38,30 +40,31 @@ const TemplateSchemeEditor: FunctionComponent<TemplateSchemeEditorProps> = ({
aria-description={ariaDescription}
/>
<div className="flex flex-wrap gap-x-2 gap-y-1 text-[10px] text-slate-400 dark:text-slate-500">
<span className="font-bold uppercase tracking-wider text-slate-500">Placeholders:</span>
<span className="font-bold uppercase tracking-wider text-slate-500">{t("Placeholders:")}</span>
{Object.keys(tokenLabels).map((token) => (
<code key={token} className="rounded bg-black/5 px-1 py-0.5 dark:bg-white/5">
{`{${token}}`}
</code>
))}
</div>
</div>
);
</div>;
};

export const BranchNameSchemeEditor: FunctionComponent<BranchNameSchemeEditorProps> = ({
value,
onChange,
disabled,
}) => {
const { locale, t } = useSettingsOperationsTranslations();
return (
<TemplateSchemeEditor
value={value}
onChange={onChange}
disabled={disabled}
placeholder="e.g. feature/sprint{sprint_id}-implementation"
ariaLabel="Sprint branch scheme"
ariaDescription="Template used when naming sprint branches."
tokenLabels={BRANCH_NAME_TOKEN_LABELS}
ariaLabel={t("Sprint branch scheme")}
ariaDescription={t("Template used when naming sprint branches.")}
tokenLabels={getBranchNameTokenLabels(locale)}
/>
);
};
Expand All @@ -70,14 +73,15 @@ export const TaskPrTitleSchemeEditor: FunctionComponent<BranchNameSchemeEditorPr
value,
onChange,
disabled,
}) => (
<TemplateSchemeEditor
}) => {
const { locale, t } = useSettingsOperationsTranslations();
return <TemplateSchemeEditor
value={value}
onChange={onChange}
disabled={disabled}
placeholder="e.g. ({sprint_tag}) {task_title}"
ariaLabel="Task PR title scheme"
ariaDescription="Template used when naming automatically-created task pull requests."
tokenLabels={TASK_PR_TITLE_TOKEN_LABELS}
/>
);
ariaLabel={t("Task PR title scheme")}
ariaDescription={t("Template used when naming automatically-created task pull requests.")}
tokenLabels={getTaskPrTitleTokenLabels(locale)}
/>;
};
26 changes: 14 additions & 12 deletions dashboard/src/v2/components/settings/LocalFilePickerField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AlertCircle, Check, ChevronUp, FileText, FolderOpen, Home, Loader2, Ref
import type { LocalFileBrowserResponse } from "../../types.js";
import { fetchLocalFiles } from "../../lib/project-api.js";
import { TextInput } from "./SettingsFormFields.js";
import { useSettingsOperationsTranslations } from "../../i18n/messages/settings-operations.js";

export const LocalFilePickerField: FunctionComponent<{
value: string;
Expand All @@ -12,6 +13,7 @@ export const LocalFilePickerField: FunctionComponent<{
helperText?: string;
placeholder?: string;
}> = ({ value, onChange, label, helperText, placeholder }) => {
const { t } = useSettingsOperationsTranslations();
const generatedId = useId();
const pickerId = `${generatedId}-picker`;
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -60,7 +62,7 @@ export const LocalFilePickerField: FunctionComponent<{
className="inline-flex shrink-0 items-center justify-center gap-2 rounded-[var(--radius-ui)] border border-[color:var(--border-hairline)] bg-void-900 px-4 py-2.5 text-xs font-black uppercase tracking-[0.14em] text-white transition-[background-color,border-color,color,box-shadow,transform] hover:-translate-y-px hover:bg-void-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-[0.98] dark:border-white/[0.08] dark:bg-white/[0.08] dark:text-white dark:hover:bg-white/[0.12] dark:focus-visible:ring-offset-void-900"
>
{isOpen ? <X aria-hidden="true" className="h-4 w-4" /> : <FolderOpen aria-hidden="true" className="h-4 w-4" />}
{isOpen ? "Close" : "Browse"}
{isOpen ? t("Close") : t("Browse")}
</button>
</div>

Expand All @@ -75,8 +77,8 @@ export const LocalFilePickerField: FunctionComponent<{
onClick={() => listing?.parentPath && void loadFiles(listing.parentPath)}
disabled={!listing?.parentPath || isLoading}
className="flex h-8 w-8 items-center justify-center rounded-xl bg-white text-slate-500 shadow-sm transition-colors hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-white/[0.06] dark:text-slate-300 dark:hover:text-white"
aria-label={`${label}: go to parent directory`}
title="Go to parent directory"
aria-label={t("{label}: go to parent directory", { label })}
title={t("Go to parent directory")}
>
<ChevronUp aria-hidden="true" className="h-4 w-4" />
</button>
Expand All @@ -85,8 +87,8 @@ export const LocalFilePickerField: FunctionComponent<{
onClick={() => void loadFiles(listing?.homePath)}
disabled={isLoading}
className="flex h-8 w-8 items-center justify-center rounded-xl bg-white text-slate-500 shadow-sm transition-colors hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-white/[0.06] dark:text-slate-300 dark:hover:text-white"
aria-label={`${label}: go to home directory`}
title="Go to home directory"
aria-label={t("{label}: go to home directory", { label })}
title={t("Go to home directory")}
>
<Home aria-hidden="true" className="h-4 w-4" />
</button>
Expand All @@ -96,14 +98,14 @@ export const LocalFilePickerField: FunctionComponent<{
disabled={isLoading}
aria-busy={isLoading}
className="flex h-8 w-8 items-center justify-center rounded-xl bg-white text-slate-500 shadow-sm transition-colors hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-white/[0.06] dark:text-slate-300 dark:hover:text-white"
aria-label={`${label}: refresh current path`}
title="Refresh current path"
aria-label={t("{label}: refresh current path", { label })}
title={t("Refresh current path")}
>
<RefreshCw aria-hidden="true" className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
{isLoading ? <span className="sr-only">Loading files</span> : null}
{isLoading ? <span className="sr-only">{t("Loading files")}</span> : null}
</button>
<div className="min-w-0 flex-1 truncate rounded-xl bg-white px-3 py-2 font-mono text-xs font-semibold text-slate-600 dark:bg-white/[0.055] dark:text-slate-300">
{listing?.currentPath || "Loading files..."}
{listing?.currentPath || t("Loading files...")}
</div>
</div>

Expand All @@ -117,7 +119,7 @@ export const LocalFilePickerField: FunctionComponent<{
{isLoading && !listing ? (
<div className="flex items-center gap-2 px-2 py-3 text-xs font-semibold text-slate-500 dark:text-slate-400">
<Loader2 aria-hidden="true" className="h-4 w-4 animate-spin" />
Loading files
{t("Loading files")}
</div>
) : listing ? (
<div className="grid gap-1">
Expand Down Expand Up @@ -146,13 +148,13 @@ export const LocalFilePickerField: FunctionComponent<{
))}
{!listing.directories.length && !listing.files.length ? (
<div className="px-2 py-3 text-xs font-semibold text-slate-500 dark:text-slate-400">
No child directories or files
{t("No child directories or files")}
</div>
) : null}
</div>
) : (
<div className="px-2 py-3 text-xs font-semibold text-slate-500 dark:text-slate-400">
Open the picker to browse local files.
{t("Open the picker to browse local files.")}
</div>
)}
</div>
Expand Down
59 changes: 32 additions & 27 deletions dashboard/src/v2/components/settings/OpenSourceSoftwareModal.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { FunctionComponent } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { ExternalLink, Search } from "lucide-preact";
import { OPEN_SOURCE_SOFTWARE, type OpenSourceSoftwareEntry } from "../../lib/open-source-software.js";
import { getOpenSourceSoftwareUsageAreaLabel, OPEN_SOURCE_SOFTWARE, type OpenSourceSoftwareEntry } from "../../lib/open-source-software.js";
import { getSafeUrl } from "../../lib/safe-url.js";
import { useInteractionTokens } from "../../lib/motion/tokens.js";
import { Input } from "../ui/Input.js";
import { Modal } from "../ui/Modal.js";
import { SHARED_INTERACTION_CLASSES } from "../ui/Button.js";
import { useSettingsOperationsTranslations } from "../../i18n/messages/settings-operations.js";

export interface OpenSourceSoftwareModalProps {
isOpen: boolean;
Expand All @@ -17,12 +18,12 @@ const TITLE_ID = "open-source-software-modal-title";
const DESCRIPTION_ID = "open-source-software-modal-description";
const SEARCH_ID = "open-source-software-search";

function matchesSearch(entry: OpenSourceSoftwareEntry, normalizedQuery: string): boolean {
function matchesSearch(entry: OpenSourceSoftwareEntry, normalizedQuery: string, localizedUsageArea: string): boolean {
if (!normalizedQuery) {
return true;
}

return [entry.name, entry.usageArea, entry.license, entry.projectUrl]
return [entry.name, entry.usageArea, localizedUsageArea, entry.license, entry.projectUrl]
.some((value) => value.toLocaleLowerCase().includes(normalizedQuery));
}

Expand All @@ -33,10 +34,15 @@ export const OpenSourceSoftwareModal: FunctionComponent<OpenSourceSoftwareModalP
const [query, setQuery] = useState("");
const searchInputRef = useRef<HTMLInputElement>(null);
const tokens = useInteractionTokens();
const { locale, t } = useSettingsOperationsTranslations();
const normalizedQuery = query.trim().toLocaleLowerCase();
const matchingEntries = useMemo(
() => OPEN_SOURCE_SOFTWARE.filter((entry) => matchesSearch(entry, normalizedQuery)),
[normalizedQuery],
() => OPEN_SOURCE_SOFTWARE.filter((entry) => matchesSearch(
entry,
normalizedQuery,
getOpenSourceSoftwareUsageAreaLabel(entry.usageArea, locale),
)),
[locale, normalizedQuery],
);

useEffect(() => {
Expand All @@ -57,17 +63,16 @@ export const OpenSourceSoftwareModal: FunctionComponent<OpenSourceSoftwareModalP
<div className="flex min-h-0 flex-1 flex-col">
<header className="shrink-0 border-b border-black/[0.06] px-4 py-5 dark:border-white/[0.06] sm:px-6">
<h2 id={TITLE_ID} className="text-xl font-bold tracking-tight text-slate-900 dark:text-white">
Open Source Software
{t("Open Source Software")}
</h2>
<p id={DESCRIPTION_ID} className="mt-1 max-w-3xl text-sm leading-relaxed text-slate-500 dark:text-slate-400">
Browse the open-source projects included in the Code UX runtime, dashboard, and packaged application.
This catalog is informational and does not change your settings.
{t("Browse the open-source projects included in the Code UX runtime, dashboard, and packaged application. This catalog is informational and does not change your settings.")}
</p>

<div className="mt-4 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div className="min-w-0 flex-1 sm:max-w-md">
<label htmlFor={SEARCH_ID} className="mb-1.5 block text-xs font-bold uppercase tracking-[0.12em] text-slate-600 dark:text-slate-300">
Search software catalog
{t("Search software catalog")}
</label>
<div className="relative">
<Search aria-hidden="true" className="pointer-events-none absolute left-3.5 top-[0.7rem] z-10 h-4 w-4 text-slate-400" />
Expand All @@ -77,14 +82,14 @@ export const OpenSourceSoftwareModal: FunctionComponent<OpenSourceSoftwareModalP
type="search"
value={query}
onInput={(event) => setQuery(event.currentTarget.value)}
placeholder="Search by project, area, or license"
placeholder={t("Search by project, area, or license")}
className="w-full min-w-0 pl-10"
autoComplete="off"
/>
</div>
</div>
<p role="status" aria-live="polite" className="pb-5 text-xs font-semibold text-slate-500 dark:text-slate-400">
{matchingEntries.length} of {OPEN_SOURCE_SOFTWARE.length} projects shown
{t("{shown} of {total} projects shown", { shown: matchingEntries.length, total: OPEN_SOURCE_SOFTWARE.length })}
</p>
</div>
</header>
Expand All @@ -96,49 +101,49 @@ export const OpenSourceSoftwareModal: FunctionComponent<OpenSourceSoftwareModalP
{matchingEntries.length > 0 ? (
<div>
<div aria-hidden="true" className="mb-2 hidden grid-cols-[minmax(0,1.4fr)_minmax(8rem,0.7fr)_minmax(0,1fr)_auto] gap-4 px-4 text-[10px] font-bold uppercase tracking-[0.16em] text-slate-400 sm:grid">
<span>Project</span>
<span>Usage area</span>
<span>License</span>
<span>Project link</span>
<span>{t("Project")}</span>
<span>{t("Usage area")}</span>
<span>{t("License")}</span>
<span>{t("Project link")}</span>
</div>
<ul role="list" aria-label="Open-source software catalog" className="space-y-2">
<ul role="list" aria-label={t("Open-source software catalog")} className="space-y-2">
{matchingEntries.map((entry) => {
const safeProjectUrl = getSafeUrl(entry.projectUrl);

return (
<li key={entry.id} className="rounded-[var(--radius-ui)] border border-[color:var(--border-hairline)] bg-[var(--surface-glass)] px-4 py-3">
<article className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-[minmax(0,1.4fr)_minmax(8rem,0.7fr)_minmax(0,1fr)_auto] sm:items-center sm:gap-4">
<div className="min-w-0">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">Project</span>
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">{t("Project")}</span>
<h3 className="break-words text-sm font-bold text-slate-900 dark:text-white">{entry.name}</h3>
</div>
<div className="min-w-0">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">Usage area</span>
<p className="break-words text-sm text-slate-600 dark:text-slate-300">{entry.usageArea}</p>
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">{t("Usage area")}</span>
<p className="break-words text-sm text-slate-600 dark:text-slate-300">{getOpenSourceSoftwareUsageAreaLabel(entry.usageArea, locale)}</p>
</div>
<div className="min-w-0">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">License</span>
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">{t("License")}</span>
<p className="break-words text-sm text-slate-600 dark:text-slate-300">{entry.license}</p>
</div>
<div className="min-w-0">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">Project link</span>
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400 sm:hidden">{t("Project link")}</span>
{safeProjectUrl ? (
<a
href={safeProjectUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Visit the ${entry.name} project website`}
aria-label={t("Visit the {name} project website", { name: entry.name })}
style={{
transitionDuration: tokens.controlFeedback.duration,
transitionTimingFunction: tokens.controlFeedback.ease,
}}
className="inline-flex items-center gap-1.5 rounded-md text-sm font-semibold text-signal-700 underline-offset-4 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-signal)] dark:text-signal-300"
>
Project website
{t("Project website")}
<ExternalLink aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
</a>
) : (
<span className="text-sm text-slate-500 dark:text-slate-400">Project website unavailable</span>
<span className="text-sm text-slate-500 dark:text-slate-400">{t("Project website unavailable")}</span>
)}
</div>
</article>
Expand All @@ -149,9 +154,9 @@ export const OpenSourceSoftwareModal: FunctionComponent<OpenSourceSoftwareModalP
</div>
) : (
<div className="flex min-h-48 flex-col items-center justify-center rounded-[var(--radius-ui)] border border-dashed border-[color:var(--border-hairline)] px-6 py-10 text-center">
<p className="text-base font-bold text-slate-800 dark:text-slate-100">No matching open-source projects</p>
<p className="text-base font-bold text-slate-800 dark:text-slate-100">{t("No matching open-source projects")}</p>
<p className="mt-1 max-w-md text-sm text-slate-500 dark:text-slate-400">
No projects match “{query.trim()}”. Try a project name, usage area, or license.
{t("No projects match “{query}”. Try a project name, usage area, or license.", { query: query.trim() })}
</p>
</div>
)}
Expand All @@ -167,7 +172,7 @@ export const OpenSourceSoftwareModal: FunctionComponent<OpenSourceSoftwareModalP
}}
className={`${SHARED_INTERACTION_CLASSES} inline-flex items-center justify-center rounded-[var(--radius-ui)] border border-[color:var(--border-hairline)] bg-[var(--surface-glass)] px-4 py-2 text-sm font-bold text-slate-700 hover:bg-[var(--surface-glass-hover)] dark:text-slate-200`}
>
Close
{t("Close")}
</button>
</footer>
</div>
Expand Down
Loading
Loading