Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: filtering keys by tag #3404

Merged
merged 8 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
14 changes: 11 additions & 3 deletions packages/core/src/Controller/Plugins/Plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,14 @@ export function Plugins(
addPlugin,
findPositions: findPositions,
run() {
const { apiKey, apiUrl, projectId, observerOptions, tagNewKeys } =
getInitialOptions();
const {
apiKey,
apiUrl,
projectId,
observerOptions,
tagNewKeys,
filterTag,
} = getInitialOptions();
instances.ui = plugins.ui?.({
apiKey: apiKey!,
apiUrl: apiUrl!,
Expand All @@ -198,6 +204,7 @@ export function Plugins(
findPositions,
onPermanentChange: (data) => events.onPermanentChange.emit(data),
tagNewKeys,
filterTag,
});

instances.observer?.run({
Expand Down Expand Up @@ -259,13 +266,14 @@ export function Plugins(
}) as BackendGetRecordInternal,

getBackendDevRecord: (async ({ language, namespace }) => {
const { apiKey, apiUrl, projectId } = getInitialOptions();
const { apiKey, apiUrl, projectId, filterTag } = getInitialOptions();
return instances.devBackend?.getRecord({
apiKey,
apiUrl,
projectId,
language,
namespace,
filterTag,
...getCommonProps(),
});
}) as BackendGetRecordInternal,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/Controller/State/initState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ export type TolgeeOptionsInternal = {
* Specify tags that will be preselected for non-existant keys.
*/
tagNewKeys?: string[];

/**
* Use only keys marked with one one of the listed tags
stepan662 marked this conversation as resolved.
Show resolved Hide resolved
*/
filterTag?: string[];
};

export type TolgeeOptions = Partial<
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type BackendDevProps = {
apiUrl?: string;
apiKey?: string;
projectId?: number | string;
filterTag?: string[];
};

export type CommonProps = {
Expand Down Expand Up @@ -160,6 +161,7 @@ export type UiProps = {
changeTranslation: ChangeTranslationInterface;
onPermanentChange: (props: TranslationDescriptor) => void;
tagNewKeys?: string[];
filterTag?: string[];
};

export type UiKeyOption = {
Expand Down
2 changes: 1 addition & 1 deletion packages/i18next/src/__integration/tolgeeUpdating.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import i18n from 'i18next';
import { DevTools, Tolgee } from '@tolgee/web';
import { withTolgee, I18nextPlugin } from '..';

const API_URL = 'http://localhost';
const API_URL = 'http://localhost/';
const API_KEY = 'dummyApiKey';

const fetch = mockCoreFetch();
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/basicTolgee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const tolgee = Tolgee()
availableLanguages: ['en', 'cs', 'fr', 'de'],
defaultLanguage: 'en',
tagNewKeys: ['draft'],
filterTag: ['test'],
});

export const useTolgee = (events?: TolgeeEvent[]): TolgeeInstance => {
Expand Down
29 changes: 22 additions & 7 deletions packages/web/src/package/DevBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,36 @@ import { getApiKeyType, getProjectIdFromApiKey } from './tools/decodeApiKey';

function createDevBackend(): BackendDevMiddleware {
return {
getRecord({ apiUrl, apiKey, language, namespace, projectId, fetch }) {
getRecord({
apiUrl,
apiKey,
language,
namespace,
projectId,
filterTag,
fetch,
}) {
const pId = getProjectIdFromApiKey(apiKey) ?? projectId;
let url =
pId !== undefined
? `${apiUrl}/v2/projects/${pId}/translations/${language}`
: `${apiUrl}/v2/projects/translations/${language}`;
const url = new URL(apiUrl);

if (pId !== undefined) {
url.pathname = `/v2/projects/${pId}/translations/${language}`;
} else {
url.pathname = `/v2/projects/translations/${language}`;
}

if (namespace) {
url += `?ns=${namespace}`;
url.searchParams.append('ns', namespace);
}
filterTag?.forEach((tag) => {
url.searchParams.append('filterTag', tag);
});

if (getApiKeyType(apiKey) === 'tgpat' && projectId === undefined) {
throw new Error("You need to specify 'projectId' when using PAT key");
}
return fetch(url, {

return fetch(url.toString(), {
headers: {
'X-API-Key': apiKey || '',
'Content-Type': 'application/json',
Expand Down
7 changes: 5 additions & 2 deletions packages/web/src/package/ui/KeyDialog/KeyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PluralFormCheckbox } from './PluralFormCheckbox';
import { ErrorAlert } from './ErrorAlert';
import { HttpError } from '../client/HttpError';
import { Tooltip } from '../common/Tooltip';
import { FilterTagMissingInfo } from './Tags/FilterTagMissingInfo';

const ScContainer = styled('div')`
font-family: Rubik, Roboto, Arial;
Expand Down Expand Up @@ -53,7 +54,7 @@ const ScKeyHint = styled('span')`
`;

const ScFieldsWrapper = styled('div')`
margin-top: 20px;
margin-top: 10px;
`;

const ScTagsWrapper = styled('div')`
Expand Down Expand Up @@ -103,6 +104,7 @@ export const KeyForm = () => {
const fallbackNamespaces = useDialogContext((c) => c.fallbackNamespaces);
const selectedNs = useDialogContext((c) => c.selectedNs);
const permissions = useDialogContext((c) => c.permissions);
const filterTagMissing = useDialogContext((c) => c.filterTagMissing);

const screenshotsView = permissions.canViewScreenshots;
const viewPluralCheckbox = permissions.canEditPlural && pluralsSupported;
Expand Down Expand Up @@ -181,6 +183,7 @@ export const KeyForm = () => {
<ScTagsWrapper>
<ScFieldTitle>Tags</ScFieldTitle>
<Tags />
{filterTagMissing && <FilterTagMissingInfo />}
</ScTagsWrapper>
)}
{ready && viewPluralCheckbox && <PluralFormCheckbox />}
Expand All @@ -207,7 +210,7 @@ export const KeyForm = () => {
</Button>
<LoadingButton
loading={saving}
disabled={saving || formDisabled}
disabled={saving || formDisabled || filterTagMissing}
onClick={onSave}
color="primary"
variant="contained"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const PluralFormCheckbox = () => {
const expanded = _expanded && isPlural;

return (
<Box display="grid" mt={2}>
<Box display="grid">
<Box justifyContent="start" display="flex" alignItems="center">
<FormControlLabel
data-cy="key-plural-checkbox"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Alert, AlertTitle, Box } from '@mui/material';
import { useDialogContext } from '../dialogContext';
import { MissingTagsList } from './MissingTagsList';

export const FilterTagMissingInfo = () => {
const filterTag = useDialogContext((c) => c.uiProps.filterTag);
const filterTagMissing = useDialogContext((c) => c.filterTagMissing);

if (!filterTagMissing) {
return null;
}

if (filterTag)
return (
<Alert severity="error" sx={{ mt: 1, mb: 1, fontSize: 15 }}>
<AlertTitle>Missing Required Tag</AlertTitle>
{filterTag.length > 1 ? (
<Box>
This app is configured to use only keys tagged with the{' '}
<MissingTagsList tags={filterTag} />. Add one of them to continue.
</Box>
) : (
<Box>
This app is configured to use only keys tagged with the{' '}
<MissingTagsList tags={filterTag} />. Add this tag to continue.
</Box>
)}
<Box mt={1}>Read more in the docs.</Box>
stepan662 marked this conversation as resolved.
Show resolved Hide resolved
</Alert>
);
};
37 changes: 37 additions & 0 deletions packages/web/src/package/ui/KeyDialog/Tags/MissingTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { styled } from '@mui/material';
import { useDialogActions } from '../dialogContext';

const StyledTag = styled('div')`
display: inline-flex;
outline: 0;
cursor: default;
padding: 1px 8px;
border-radius: 12px;
align-items: center;
font-size: 14px;
background: ${({ theme }) => theme.palette.grey[200]};
border: 1px solid transparent;
max-width: 100%;
box-sizing: border-box;
border: 1px solid ${({ theme }) => theme.palette.text.secondary};
margin: -2px 0px;
cursor: pointer;
&:focus-within,
&:hover {
border: 1px solid ${({ theme }) => theme.palette.primary.main};
color: ${({ theme }) => theme.palette.primary.main};
}
`;

type Props = {
name: string;
};

export const MissingTag = ({ name }: Props) => {
const { setTags } = useDialogActions();
function addTag(name: string) {
setTags((values) => [...values.filter((t) => t !== name), name]);
}

return <StyledTag onClick={() => addTag(name)}>{name}</StyledTag>;
};
35 changes: 35 additions & 0 deletions packages/web/src/package/ui/KeyDialog/Tags/MissingTagsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { MissingTag } from './MissingTag';

type Props = {
tags: string[];
};

export const MissingTagsList = ({ tags }: Props) => {
return (
<>
{tags.map((tag, index) => {
if (index === 0) {
return (
<React.Fragment key={tag}>
<MissingTag name={tag} />
</React.Fragment>
);
} else if (index < tags.length - 1) {
return (
<React.Fragment key={tag}>
, <MissingTag name={tag} />
</React.Fragment>
);
} else {
return (
<React.Fragment key={tag}>
{' '}
or <MissingTag name={tag} />
</React.Fragment>
);
}
})}
</>
);
};
14 changes: 11 additions & 3 deletions packages/web/src/package/ui/KeyDialog/dialogContext/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,15 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
const [saving, setSaving] = useState(false);

const [selectedNs, setSelectedNs] = useState<string>(props.namespace);
const [tags, setTags] = useState<string[]>([]);
const [tags, setTags] = useState<string[] | undefined>(undefined);
const [_isPlural, setIsPlural] = useState<boolean>();
const [_pluralArgName, setPluralArgName] = useState<string>();
const [submitError, setSubmitError] = useState<HttpError>();

const filterTagMissing =
Boolean(props.uiProps.filterTag?.length) &&
tags &&
!props.uiProps.filterTag.find((t) => tags.includes(t));
useEffect(() => {
// reset when key changes
setIsPlural(undefined);
Expand Down Expand Up @@ -182,7 +186,10 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
if (firstKey) {
setTags(firstKey?.keyTags?.map((t) => t.name) || []);
} else {
setTags(props.uiProps.tagNewKeys ?? []);
setTags([
...(props.uiProps.filterTag ?? []),
...(props.uiProps.tagNewKeys ?? []),
]);
}
setScreenshots(
firstKey?.screenshots?.map((sc) => ({
Expand Down Expand Up @@ -486,7 +493,7 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
screenshotDetail,
linkToPlatform,
keyExists,
tags,
tags: tags || [],
permissions,
canTakeScreenshots,
isPlural,
Expand All @@ -495,6 +502,7 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
pluralsSupported,
icuPlaceholders,
submitError,
filterTagMissing,
} as const;

const actions = {
Expand Down