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

[DataCleanup] Implement changes, completed for ReviewEntries #2743

Merged
merged 12 commits into from
Nov 30, 2023
28 changes: 28 additions & 0 deletions Backend.Tests/Controllers/WordControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,34 @@ public async Task TestIsFrontierNonemptyMissingProject()
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
}

[Test]
public async Task TestIsInFrontier()
{
var wordNotInFrontier = await _wordRepo.Add(Util.RandomWord(_projId));
var falseResult = (ObjectResult)await _wordController.IsInFrontier(_projId, wordNotInFrontier.Id);
Assert.That(falseResult.Value, Is.False);

var wordInFrontier = await _wordRepo.AddFrontier(Util.RandomWord(_projId));
var trueResult = (ObjectResult)await _wordController.IsInFrontier(_projId, wordInFrontier.Id);
Assert.That(trueResult.Value, Is.True);
}

[Test]
public async Task TestIsInFrontierNoPermission()
{
var wordInFrontier = await _wordRepo.AddFrontier(Util.RandomWord(_projId));
_wordController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext();
var result = await _wordController.IsInFrontier(_projId, wordInFrontier.Id);
Assert.That(result, Is.InstanceOf<ForbidResult>());
}

[Test]
public async Task TestIsInFrontierMissingProject()
{
var result = await _wordController.IsInFrontier(MissingId, "anything");
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
}

[Test]
public async Task TestGetFrontier()
{
Expand Down
17 changes: 17 additions & 0 deletions Backend/Controllers/WordController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ public async Task<IActionResult> GetProjectFrontierWords(string projectId)
return Ok(await _wordRepo.GetFrontier(projectId));
}

/// <summary> Checks if Frontier has <see cref="Word"/> in specified <see cref="Project"/>. </summary>
[HttpGet("isinfrontier/{wordId}", Name = "IsInFrontier")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(bool))]
public async Task<IActionResult> IsInFrontier(string projectId, string wordId)
{
if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry, projectId))
{
return Forbid();
}
var project = await _projRepo.GetProject(projectId);
if (project is null)
{
return NotFound(projectId);
}
return Ok(await _wordRepo.IsInFrontier(projectId, wordId));
}

/// <summary>
/// Checks if a <see cref="Word"/> is a duplicate--i.e., are its primary text fields
/// (Vernacular, Gloss text, Definition text) contained in a frontier entry?
Expand Down
15 changes: 15 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,14 @@
"toolbar": {
"search": "Search"
}
},
"completed": {
"number": "Number of entries edited: "
},
"undo": {
"undo": "Undo Edit",
"undoDialog": "Undo this edit?",
"undoDisabled": "Undo Unavailable"
}
},
"charInventory": {
Expand Down Expand Up @@ -482,5 +490,12 @@
"Other": "Other",
"Unspecified": "Unspecified"
}
},
"wordCard": {
"senseCount": "Senses: {{ val }}",
"wordId": "Id: {{ val }}",
"wordModified": "Modified: {{ val }}",
"domainAdded": "Added: {{ val }}",
"user": "By user: {{ val }}"
}
}
132 changes: 132 additions & 0 deletions src/api/api/word-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,55 @@ export const WordApiAxiosParamCreator = function (
options: localVarRequestOptions,
};
},
/**
*
* @param {string} projectId
* @param {string} wordId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
isInFrontier: async (
projectId: string,
wordId: string,
options: any = {}
): Promise<RequestArgs> => {
// verify required parameter 'projectId' is not null or undefined
assertParamExists("isInFrontier", "projectId", projectId);
// verify required parameter 'wordId' is not null or undefined
assertParamExists("isInFrontier", "wordId", wordId);
const localVarPath =
`/v1/projects/{projectId}/words/isinfrontier/{wordId}`
.replace(`{${"projectId"}}`, encodeURIComponent(String(projectId)))
.replace(`{${"wordId"}}`, encodeURIComponent(String(wordId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}

const localVarRequestOptions = {
method: "GET",
...baseOptions,
...options,
};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;

setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
};

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} projectId
Expand Down Expand Up @@ -692,6 +741,32 @@ export const WordApiFp = function (configuration?: Configuration) {
configuration
);
},
/**
*
* @param {string} projectId
* @param {string} wordId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async isInFrontier(
projectId: string,
wordId: string,
options?: any
): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<boolean>
> {
const localVarAxiosArgs = await localVarAxiosParamCreator.isInFrontier(
projectId,
wordId,
options
);
return createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration
);
},
/**
*
* @param {string} projectId
Expand Down Expand Up @@ -870,6 +945,22 @@ export const WordApiFactory = function (
.isFrontierNonempty(projectId, options)
.then((request) => request(axios, basePath));
},
/**
*
* @param {string} projectId
* @param {string} wordId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
isInFrontier(
projectId: string,
wordId: string,
options?: any
): AxiosPromise<boolean> {
return localVarFp
.isInFrontier(projectId, wordId, options)
.then((request) => request(axios, basePath));
},
/**
*
* @param {string} projectId
Expand Down Expand Up @@ -1035,6 +1126,27 @@ export interface WordApiIsFrontierNonemptyRequest {
readonly projectId: string;
}

/**
* Request parameters for isInFrontier operation in WordApi.
* @export
* @interface WordApiIsInFrontierRequest
*/
export interface WordApiIsInFrontierRequest {
/**
*
* @type {string}
* @memberof WordApiIsInFrontier
*/
readonly projectId: string;

/**
*
* @type {string}
* @memberof WordApiIsInFrontier
*/
readonly wordId: string;
}

/**
* Request parameters for updateDuplicate operation in WordApi.
* @export
Expand Down Expand Up @@ -1215,6 +1327,26 @@ export class WordApi extends BaseAPI {
.then((request) => request(this.axios, this.basePath));
}

/**
*
* @param {WordApiIsInFrontierRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof WordApi
*/
public isInFrontier(
requestParameters: WordApiIsInFrontierRequest,
options?: any
) {
return WordApiFp(this.configuration)
.isInFrontier(
requestParameters.projectId,
requestParameters.wordId,
options
)
.then((request) => request(this.axios, this.basePath));
}

/**
*
* @param {WordApiUpdateDuplicateRequest} requestParameters Request parameters.
Expand Down
9 changes: 9 additions & 0 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,15 @@ export async function isFrontierNonempty(projectId?: string): Promise<boolean> {
return (await wordApi.isFrontierNonempty(params, defaultOptions())).data;
}

export async function isInFrontier(
wordId: string,
projectId?: string
): Promise<boolean> {
projectId = projectId || LocalStorage.getProjectId();
const params = { projectId, wordId };
return (await wordApi.isInFrontier(params, defaultOptions())).data;
}

export async function updateDuplicate(
dupId: string,
word: Word
Expand Down
2 changes: 1 addition & 1 deletion src/components/App/DefaultState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { defaultState as pronunciationsState } from "components/Pronunciations/R
import { defaultState as treeViewState } from "components/TreeView/Redux/TreeViewReduxTypes";
import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes";
import { defaultState as mergeDuplicateGoal } from "goals/MergeDuplicates/Redux/MergeDupsReducer";
import { defaultState as reviewEntriesState } from "goals/ReviewEntries/ReviewEntriesComponent/Redux/ReviewEntriesReduxTypes";
import { defaultState as reviewEntriesState } from "goals/ReviewEntries/Redux/ReviewEntriesReduxTypes";
import { defaultState as analyticsState } from "types/Redux/analyticsReduxTypes";

export const defaultState = {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Buttons/FlagButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { themeColors } from "types/theme";

interface FlagButtonProps {
flag: Flag;
buttonId: string;
buttonId?: string;
updateFlag?: (flag: Flag) => void;
}

Expand Down Expand Up @@ -56,7 +56,7 @@ export default function FlagButton(props: FlagButtonProps): ReactElement {
onClick={
props.updateFlag ? () => setOpen(true) : active ? () => {} : undefined
}
buttonId={props.buttonId}
buttonId={props.buttonId ?? "flag-button"}
side="top"
/>
{props.updateFlag && (
Expand Down
61 changes: 61 additions & 0 deletions src/components/Buttons/UndoButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Button, Grid } from "@mui/material";
import { ReactElement, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";

import { CancelConfirmDialog } from "components/Dialogs";

interface UndoButtonProps {
buttonIdEnabled?: string;
buttonIdCancel?: string;
buttonIdConfirm?: string;
textIdDialog: string;
textIdDisabled: string;
textIdEnabled: string;
isUndoAllowed: () => Promise<boolean>;
undo: () => Promise<void>;
}

export default function UndoButton(props: UndoButtonProps): ReactElement {
const isUndoAllowed = props.isUndoAllowed;

const [isUndoEnabled, setUndoEnabled] = useState(false);
const [undoDialogOpen, setUndoDialogOpen] = useState(false);

const { t } = useTranslation();

useEffect(() => {
if (!undoDialogOpen) {
isUndoAllowed().then(setUndoEnabled);
}
}, [isUndoAllowed, undoDialogOpen]);

return (
<Grid container direction="column" justifyContent="center">
{isUndoEnabled ? (
<div>
<Button
variant="outlined"
id={props.buttonIdEnabled}
onClick={() => setUndoDialogOpen(true)}
>
{t(props.textIdEnabled)}
</Button>
<CancelConfirmDialog
open={undoDialogOpen}
textId={props.textIdDialog}
handleCancel={() => setUndoDialogOpen(false)}
handleConfirm={() =>
props.undo().then(() => setUndoDialogOpen(false))
}
buttonIdCancel={props.buttonIdCancel}
buttonIdConfirm={props.buttonIdConfirm}
/>
</div>
) : (
<div>
<Button disabled>{t(props.textIdDisabled)}</Button>
</div>
)}
</Grid>
);
}
2 changes: 2 additions & 0 deletions src/components/Buttons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import IconButtonWithTooltip from "components/Buttons/IconButtonWithTooltip";
import LoadingButton from "components/Buttons/LoadingButton";
import LoadingDoneButton from "components/Buttons/LoadingDoneButton";
import PartOfSpeechButton from "components/Buttons/PartOfSpeechButton";
import UndoButton from "components/Buttons/UndoButton";

export {
FileInputButton,
Expand All @@ -12,4 +13,5 @@ export {
LoadingButton,
LoadingDoneButton,
PartOfSpeechButton,
UndoButton,
};
Loading