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

Added new parameter priority at Category model to modify order to be shown #1368

Merged
8 changes: 6 additions & 2 deletions airone/lib/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,13 @@ def add_entry(self, user: User, name: str, schema: Entity, values={}, is_public=

return entry

def create_category(self, user: User, name: str, note: str = "", models: List[Entity] = []):
def create_category(
self, user: User, name: str, note: str = "", models: List[Entity] = [], priority=0
) -> Category:
# create target Category instance
category = Category.objects.create(name=name, note=note, created_user=user)
category = Category.objects.create(
name=name, note=note, priority=priority, created_user=user
)

# attach created category to each specified Models
for model in models:
Expand Down
2 changes: 1 addition & 1 deletion apiclient/typescript-fetch/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dmm-com/airone-apiclient-typescript-fetch",
"version": "0.4.0",
"version": "0.5.0",
"description": "AirOne APIv2 client in TypeScript",
"main": "src/autogenerated/index.ts",
"scripts": {
Expand Down
20 changes: 9 additions & 11 deletions category/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class CategoryListSerializer(serializers.ModelSerializer):

class Meta:
model = Category
fields = ["id", "name", "note", "models"]
fields = ["id", "name", "note", "models", "priority"]


class CategoryCreateSerializer(serializers.ModelSerializer):
Expand All @@ -28,7 +28,7 @@ class CategoryCreateSerializer(serializers.ModelSerializer):

class Meta:
model = Category
fields = ["id", "name", "note", "models"]
fields = ["id", "name", "note", "models", "priority"]

def create(self, validated_data):
# craete Category instance
Expand All @@ -51,19 +51,17 @@ class CategoryUpdateSerializer(serializers.ModelSerializer):

class Meta:
model = Category
fields = ["name", "note", "models"]
fields = ["name", "note", "models", "priority"]

def update(self, category: Category, validated_data):
# name and note are updated
updated_fields: list[str] = []
category_name = validated_data.get("name", category.name)
if category_name != category.name:
category.name = category_name
updated_fields.append("name")
category_note = validated_data.get("note", category.note)
if category_note != category.note:
category.note = category_note
updated_fields.append("note")

for key in ["name", "note", "priority"]:
content = validated_data.get(key, getattr(category, key))
if content != getattr(category, key):
setattr(category, key, content)
updated_fields.append(key)

if len(updated_fields) > 0:
category.save(update_fields=updated_fields)
Expand Down
4 changes: 2 additions & 2 deletions category/api_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class CategoryAPI(viewsets.ModelViewSet):
pagination_class = LimitOffsetPagination
permission_classes = [IsAuthenticated & EntityPermission]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter]
search_fields = ["name"]
ordering = ["name"]
search_fields = ["name", "models__name"]
ordering = ["-priority", "name"]

def get_serializer_class(self):
serializer = {
Expand Down
5 changes: 5 additions & 0 deletions category/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

class Category(ACLBase):
note = models.CharField(max_length=500, blank=True, default="")
priority = models.IntegerField(default=0)

@classmethod
def all(kls):
return Category.objects.order_by("-priority")

def __init__(self, *args, **kwargs):
super(Category, self).__init__(*args, **kwargs)
Expand Down
6 changes: 6 additions & 0 deletions category/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_list(self):
"name": x.name,
"note": x.note,
"models": [{"id": model.id, "name": model.name, "is_public": True}],
"priority": x.priority,
}
for x in categories
],
Expand Down Expand Up @@ -80,6 +81,7 @@ def test_get(self):
"name": category.name,
"note": category.note,
"models": [{"id": x.id, "name": x.name, "is_public": True} for x in models],
"priority": category.priority,
},
)

Expand All @@ -95,6 +97,7 @@ def test_create(self):
"name": "New Category",
"note": "Hoge",
"models": [{"id": x.id, "name": x.name} for x in models],
"priority": 100,
}
resp = self.client.post("/category/api/v2/", json.dumps(params), "application/json")
self.assertEqual(resp.status_code, 201)
Expand All @@ -104,6 +107,7 @@ def test_create(self):
self.assertEqual(category.id, resp.json()["id"])
self.assertEqual(category.name, resp.json()["name"])
self.assertEqual(category.note, resp.json()["note"])
self.assertEqual(category.priority, resp.json()["priority"])
self.assertEqual(category.models.count(), 3)
self.assertEqual(list(category.models.all()), models)

Expand Down Expand Up @@ -150,6 +154,7 @@ def test_update(self):
"name": "Updated Category",
"note": "Updated Note",
"models": [{"id": models[i].id, "name": models[i].name} for i in [1, 2]],
"priority": 100,
}
resp = self.client.put(
"/category/api/v2/%s/" % category.id, json.dumps(params), "application/json"
Expand All @@ -173,6 +178,7 @@ def test_update_unauthorized_category(self):
"name": "Updated Category",
"note": "Updated Note",
"models": [],
"priority": 100,
}
resp = self.client.put(
"/category/api/v2/%s/" % category.id, json.dumps(params), "application/json"
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/components/category/CategoryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
isOptionEqualToValue={(option, value) =>
option.id === value.id
}
onChange={(_e, value: any) => {

Check warning on line 119 in frontend/src/components/category/CategoryForm.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
setValue("models", value, {
shouldDirty: true,
shouldValidate: true,
Expand All @@ -137,6 +137,30 @@
/>
</TableCell>
</StyledTableRow>
<StyledTableRow>
<TableCell>表示優先度</TableCell>
<TableCell>
<Controller
name="priority"
control={control}
defaultValue={0}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
type="number"
id="category-priority"
required
placeholder="表示優先度"
error={error != null}
helperText={error?.message}
size="small"
fullWidth
inputProps={{ "data-1p-ignore": true }}
/>
)}
/>
</TableCell>
</StyledTableRow>
</TableBody>
</Table>
</Box>
Expand Down
119 changes: 119 additions & 0 deletions frontend/src/components/category/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import AddIcon from "@mui/icons-material/Add";
import { Box, Button, Container, Grid, List, Typography } from "@mui/material";
import React, { FC, useState } from "react";
import { Link, useNavigate } from "react-router";

import { CategoryListHeader } from "components/category/CategoryListHeader";
import { AironeLink } from "components/common";
import { PaginationFooter } from "components/common/PaginationFooter";
import { SearchBox } from "components/common/SearchBox";
import { useAsyncWithThrow } from "hooks";
import { usePage } from "hooks/usePage";
import { aironeApiClient } from "repository";
import { entityEntriesPath, newCategoryPath } from "routes/Routes";
import { EntityListParam } from "services/Constants";
import { normalizeToMatch } from "services/StringUtil";

interface Props {
isEdit?: boolean;
}

export const CategoryList: FC<Props> = ({ isEdit = false }) => {
const navigate = useNavigate();
const [toggle, setToggle] = useState(false);

// variable to store search query
const params = new URLSearchParams(location.search);
const [query, setQuery] = useState<string>(params.get("query") ?? "");
const [page, changePage] = usePage();

// request handler when user specify query
const handleChangeQuery = (newQuery?: string) => {
changePage(1);
setQuery(newQuery ?? "");

navigate({
pathname: location.pathname,
search: newQuery ? `?query=${newQuery}` : "",
});
};

const categories = useAsyncWithThrow(async () => {
return await aironeApiClient.getCategories(page, query);
}, [page, query, toggle]);

return (
<Container>
{/* Show control menus to filter and create category */}
<Box display="flex" justifyContent="space-between" mb="16px">
<Box width="600px">
<SearchBox
placeholder="カテゴリを絞り込む"
defaultValue={query}
onKeyPress={(e) => {
e.key === "Enter" &&
handleChangeQuery(
normalizeToMatch((e.target as HTMLInputElement).value ?? "")
);
}}
/>
</Box>
{isEdit && (
<Button
variant="contained"
color="secondary"
component={Link}
to={newCategoryPath()}
sx={{ height: "48px", borderRadius: "24px" }}
>
<AddIcon /> 新規カテゴリを作成
</Button>
)}
</Box>

{/* Context of Category */}
<Grid container spacing={3}>
{categories.value?.results.map((category) => (
<Grid item md={4} key={category.id}>
<List
subheader={
<CategoryListHeader
category={category}
setToggle={() => setToggle(!toggle)}
isEdit={isEdit}
/>
}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
//overflowY: "scroll",
//maxHeight: 300,
ml: "40px",
}}
>
{category.models.map((models) => (
<Typography
key={models.id}
component={AironeLink}
to={entityEntriesPath(models.id)}
variant="body2"
>
{models.name}
</Typography>
))}
</Box>
</List>
</Grid>
))}
</Grid>
<PaginationFooter
count={categories.value?.count ?? 0}
maxRowCount={EntityListParam.MAX_ROW_COUNT}
page={page}
changePage={changePage}
/>
</Container>
);
};
65 changes: 42 additions & 23 deletions frontend/src/components/category/CategoryListHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,62 @@
import { CategoryList } from "@dmm-com/airone-apiclient-typescript-fetch";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import { IconButton, Typography } from "@mui/material";
import { Box, IconButton, Typography } from "@mui/material";
import React, { FC, useState } from "react";

import { CategoryControlMenu } from "components/category/CategoryControlMenu";
import { BetweenAlignedBox } from "components/common/FlexBox";
import { BetweenAlignedBox, FlexBox } from "components/common/FlexBox";

interface Props {
category: CategoryList;
setToggle?: () => void;
isEdit: boolean;
setToggle: () => void;
}

export const CategoryListHeader: FC<Props> = ({ category, setToggle }) => {
export const CategoryListHeader: FC<Props> = ({
category,
isEdit,
setToggle,
}) => {
const [categoryAnchorEl, setCategoryAnchorEl] =
useState<HTMLButtonElement | null>(null);

return (
<BetweenAlignedBox>
{/* Category title */}
<Typography variant="h6" component="div">
{category.name}
</Typography>
{/* Category image */}
<FlexBox alignItems="center">
<Box
mr="8px"
p="4px"
height="24px"
width="24px"
component="img"
src="/static/images/category/01.png"
/>

{/* Category title */}
<Typography variant="h6" component="div">
{category.name}
</Typography>
</FlexBox>

{/* Category control menu */}
<>
<IconButton
onClick={(e) => {
setCategoryAnchorEl(e.currentTarget);
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
<CategoryControlMenu
categoryId={category.id}
anchorElem={categoryAnchorEl}
handleClose={() => setCategoryAnchorEl(null)}
setToggle={setToggle}
/>
</>
{isEdit && (
<FlexBox sx={{ marginInlineStart: "40px" }}>
<IconButton
onClick={(e) => {
setCategoryAnchorEl(e.currentTarget);
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
<CategoryControlMenu
categoryId={category.id}
anchorElem={categoryAnchorEl}
handleClose={() => setCategoryAnchorEl(null)}
setToggle={setToggle}
/>
</FlexBox>
)}
</BetweenAlignedBox>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const schema = schemaForType<CategoryList>()(
})
)
.default([]),
//priority: z.number().default(0).refine((v) => Number(v)),
priority: z.coerce.number(),
})
);

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/CategoryEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export const CategoryEditPage: FC = () => {

const handleSubmitOnValid = useCallback(
async (category: Schema) => {
// Note: This might not necessary any more
category = { ...category, priority: Number(category.priority) };
try {
if (willCreate) {
await aironeApiClient.createCategory(category);
Expand Down
Loading
Loading