diff --git a/openapi.yaml b/openapi.yaml index c87c0d4..fb14d98 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -447,6 +447,30 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /mapfixes/{MapfixID}/description: + patch: + summary: Update description (submitter only) + operationId: updateMapfixDescription + tags: + - Mapfixes + parameters: + - $ref: '#/components/parameters/MapfixID' + requestBody: + required: true + content: + text/plain: + schema: + type: string + maxLength: 256 + responses: + "204": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /mapfixes/{MapfixID}/completed: post: summary: Called by maptest when a player completes the map diff --git a/pkg/api/oas_client_gen.go b/pkg/api/oas_client_gen.go index 556c689..be72604 100644 --- a/pkg/api/oas_client_gen.go +++ b/pkg/api/oas_client_gen.go @@ -385,6 +385,12 @@ type Invoker interface { // // POST /submissions/{SubmissionID}/completed SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error + // UpdateMapfixDescription invokes updateMapfixDescription operation. + // + // Update description (submitter only). + // + // PATCH /mapfixes/{MapfixID}/description + UpdateMapfixDescription(ctx context.Context, request UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error // UpdateMapfixModel invokes updateMapfixModel operation. // // Update model following role restrictions. @@ -7701,6 +7707,134 @@ func (c *Client) sendSetSubmissionCompleted(ctx context.Context, params SetSubmi return result, nil } +// UpdateMapfixDescription invokes updateMapfixDescription operation. +// +// Update description (submitter only). +// +// PATCH /mapfixes/{MapfixID}/description +func (c *Client) UpdateMapfixDescription(ctx context.Context, request UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error { + _, err := c.sendUpdateMapfixDescription(ctx, request, params) + return err +} + +func (c *Client) sendUpdateMapfixDescription(ctx context.Context, request UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) (res *UpdateMapfixDescriptionNoContent, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("updateMapfixDescription"), + semconv.HTTPRequestMethodKey.String("PATCH"), + semconv.URLTemplateKey.String("/mapfixes/{MapfixID}/description"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, UpdateMapfixDescriptionOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/mapfixes/" + { + // Encode "MapfixID" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "MapfixID", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.Int64ToString(params.MapfixID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/description" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "PATCH", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeUpdateMapfixDescriptionRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:CookieAuth" + switch err := c.securityCookieAuth(ctx, UpdateMapfixDescriptionOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"CookieAuth\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeUpdateMapfixDescriptionResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // UpdateMapfixModel invokes updateMapfixModel operation. // // Update model following role restrictions. diff --git a/pkg/api/oas_handlers_gen.go b/pkg/api/oas_handlers_gen.go index e1b6c5e..7373e99 100644 --- a/pkg/api/oas_handlers_gen.go +++ b/pkg/api/oas_handlers_gen.go @@ -11020,6 +11020,219 @@ func (s *Server) handleSetSubmissionCompletedRequest(args [1]string, argsEscaped } } +// handleUpdateMapfixDescriptionRequest handles updateMapfixDescription operation. +// +// Update description (submitter only). +// +// PATCH /mapfixes/{MapfixID}/description +func (s *Server) handleUpdateMapfixDescriptionRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("updateMapfixDescription"), + semconv.HTTPRequestMethodKey.String("PATCH"), + semconv.HTTPRouteKey.String("/mapfixes/{MapfixID}/description"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), UpdateMapfixDescriptionOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: UpdateMapfixDescriptionOperation, + ID: "updateMapfixDescription", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityCookieAuth(ctx, UpdateMapfixDescriptionOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "CookieAuth", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security:CookieAuth", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeUpdateMapfixDescriptionParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var rawBody []byte + request, rawBody, close, err := s.decodeUpdateMapfixDescriptionRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response *UpdateMapfixDescriptionNoContent + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: UpdateMapfixDescriptionOperation, + OperationSummary: "Update description (submitter only)", + OperationID: "updateMapfixDescription", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{ + { + Name: "MapfixID", + In: "path", + }: params.MapfixID, + }, + Raw: r, + } + + type ( + Request = UpdateMapfixDescriptionReq + Params = UpdateMapfixDescriptionParams + Response = *UpdateMapfixDescriptionNoContent + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackUpdateMapfixDescriptionParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.UpdateMapfixDescription(ctx, request, params) + return response, err + }, + ) + } else { + err = s.h.UpdateMapfixDescription(ctx, request, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeUpdateMapfixDescriptionResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleUpdateMapfixModelRequest handles updateMapfixModel operation. // // Update model following role restrictions. diff --git a/pkg/api/oas_operations_gen.go b/pkg/api/oas_operations_gen.go index 801ae8f..260e79e 100644 --- a/pkg/api/oas_operations_gen.go +++ b/pkg/api/oas_operations_gen.go @@ -65,6 +65,7 @@ const ( SessionValidateOperation OperationName = "SessionValidate" SetMapfixCompletedOperation OperationName = "SetMapfixCompleted" SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted" + UpdateMapfixDescriptionOperation OperationName = "UpdateMapfixDescription" UpdateMapfixModelOperation OperationName = "UpdateMapfixModel" UpdateScriptOperation OperationName = "UpdateScript" UpdateScriptPolicyOperation OperationName = "UpdateScriptPolicy" diff --git a/pkg/api/oas_parameters_gen.go b/pkg/api/oas_parameters_gen.go index b6b84b5..8703b44 100644 --- a/pkg/api/oas_parameters_gen.go +++ b/pkg/api/oas_parameters_gen.go @@ -6818,6 +6818,90 @@ func decodeSetSubmissionCompletedParams(args [1]string, argsEscaped bool, r *htt return params, nil } +// UpdateMapfixDescriptionParams is parameters of updateMapfixDescription operation. +type UpdateMapfixDescriptionParams struct { + // The unique identifier for a mapfix. + MapfixID int64 +} + +func unpackUpdateMapfixDescriptionParams(packed middleware.Parameters) (params UpdateMapfixDescriptionParams) { + { + key := middleware.ParameterKey{ + Name: "MapfixID", + In: "path", + } + params.MapfixID = packed[key].(int64) + } + return params +} + +func decodeUpdateMapfixDescriptionParams(args [1]string, argsEscaped bool, r *http.Request) (params UpdateMapfixDescriptionParams, _ error) { + // Decode path: MapfixID. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "MapfixID", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.MapfixID = c + return nil + }(); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + Pattern: nil, + }).Validate(int64(params.MapfixID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "MapfixID", + In: "path", + Err: err, + } + } + return params, nil +} + // UpdateMapfixModelParams is parameters of updateMapfixModel operation. type UpdateMapfixModelParams struct { // The unique identifier for a mapfix. diff --git a/pkg/api/oas_request_decoders_gen.go b/pkg/api/oas_request_decoders_gen.go index b0c672f..ce7cdea 100644 --- a/pkg/api/oas_request_decoders_gen.go +++ b/pkg/api/oas_request_decoders_gen.go @@ -829,6 +829,41 @@ func (s *Server) decodeReleaseSubmissionsRequest(r *http.Request) ( } } +func (s *Server) decodeUpdateMapfixDescriptionRequest(r *http.Request) ( + req UpdateMapfixDescriptionReq, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "text/plain": + reader := r.Body + request := UpdateMapfixDescriptionReq{Data: reader} + return request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeUpdateScriptRequest(r *http.Request) ( req *ScriptUpdate, rawBody []byte, diff --git a/pkg/api/oas_request_encoders_gen.go b/pkg/api/oas_request_encoders_gen.go index 016d63d..177c610 100644 --- a/pkg/api/oas_request_encoders_gen.go +++ b/pkg/api/oas_request_encoders_gen.go @@ -160,6 +160,16 @@ func encodeReleaseSubmissionsRequest( return nil } +func encodeUpdateMapfixDescriptionRequest( + req UpdateMapfixDescriptionReq, + r *http.Request, +) error { + const contentType = "text/plain" + body := req + ht.SetBody(r, body, contentType) + return nil +} + func encodeUpdateScriptRequest( req *ScriptUpdate, r *http.Request, diff --git a/pkg/api/oas_response_decoders_gen.go b/pkg/api/oas_response_decoders_gen.go index 2670b85..fe3a927 100644 --- a/pkg/api/oas_response_decoders_gen.go +++ b/pkg/api/oas_response_decoders_gen.go @@ -4808,6 +4808,66 @@ func decodeSetSubmissionCompletedResponse(resp *http.Response) (res *SetSubmissi return res, errors.Wrap(defRes, "error") } +func decodeUpdateMapfixDescriptionResponse(resp *http.Response) (res *UpdateMapfixDescriptionNoContent, _ error) { + switch resp.StatusCode { + case 204: + // Code 204. + return &UpdateMapfixDescriptionNoContent{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeUpdateMapfixModelResponse(resp *http.Response) (res *UpdateMapfixModelNoContent, _ error) { switch resp.StatusCode { case 204: diff --git a/pkg/api/oas_response_encoders_gen.go b/pkg/api/oas_response_encoders_gen.go index c32470c..ea5dbc8 100644 --- a/pkg/api/oas_response_encoders_gen.go +++ b/pkg/api/oas_response_encoders_gen.go @@ -677,6 +677,13 @@ func encodeSetSubmissionCompletedResponse(response *SetSubmissionCompletedNoCont return nil } +func encodeUpdateMapfixDescriptionResponse(response *UpdateMapfixDescriptionNoContent, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(204) + span.SetStatus(codes.Ok, http.StatusText(204)) + + return nil +} + func encodeUpdateMapfixModelResponse(response *UpdateMapfixModelNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) diff --git a/pkg/api/oas_router_gen.go b/pkg/api/oas_router_gen.go index df6d20c..23dde03 100644 --- a/pkg/api/oas_router_gen.go +++ b/pkg/api/oas_router_gen.go @@ -216,6 +216,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } + case 'd': // Prefix: "description" + + if l := len("description"); len(elem) >= l && elem[0:l] == "description" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "PATCH": + s.handleUpdateMapfixDescriptionRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "PATCH") + } + + return + } + case 'm': // Prefix: "model" if l := len("model"); len(elem) >= l && elem[0:l] == "model" { @@ -1894,6 +1916,31 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } + case 'd': // Prefix: "description" + + if l := len("description"); len(elem) >= l && elem[0:l] == "description" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "PATCH": + r.name = UpdateMapfixDescriptionOperation + r.summary = "Update description (submitter only)" + r.operationID = "updateMapfixDescription" + r.operationGroup = "" + r.pathPattern = "/mapfixes/{MapfixID}/description" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + case 'm': // Prefix: "model" if l := len("model"); len(elem) >= l && elem[0:l] == "model" { diff --git a/pkg/api/oas_schemas_gen.go b/pkg/api/oas_schemas_gen.go index 5af154d..a05861d 100644 --- a/pkg/api/oas_schemas_gen.go +++ b/pkg/api/oas_schemas_gen.go @@ -2308,6 +2308,23 @@ func (s *Submissions) SetSubmissions(val []Submission) { s.Submissions = val } +// UpdateMapfixDescriptionNoContent is response for UpdateMapfixDescription operation. +type UpdateMapfixDescriptionNoContent struct{} + +type UpdateMapfixDescriptionReq struct { + Data io.Reader +} + +// Read reads data from the Data reader. +// +// Kept to satisfy the io.Reader interface. +func (s UpdateMapfixDescriptionReq) Read(p []byte) (n int, err error) { + if s.Data == nil { + return 0, io.EOF + } + return s.Data.Read(p) +} + // UpdateMapfixModelNoContent is response for UpdateMapfixModel operation. type UpdateMapfixModelNoContent struct{} diff --git a/pkg/api/oas_security_gen.go b/pkg/api/oas_security_gen.go index 86db301..fe7d2cc 100644 --- a/pkg/api/oas_security_gen.go +++ b/pkg/api/oas_security_gen.go @@ -74,6 +74,7 @@ var operationRolesCookieAuth = map[string][]string{ SessionValidateOperation: []string{}, SetMapfixCompletedOperation: []string{}, SetSubmissionCompletedOperation: []string{}, + UpdateMapfixDescriptionOperation: []string{}, UpdateMapfixModelOperation: []string{}, UpdateScriptOperation: []string{}, UpdateScriptPolicyOperation: []string{}, diff --git a/pkg/api/oas_server_gen.go b/pkg/api/oas_server_gen.go index e944a2e..dc3be49 100644 --- a/pkg/api/oas_server_gen.go +++ b/pkg/api/oas_server_gen.go @@ -365,6 +365,12 @@ type Handler interface { // // POST /submissions/{SubmissionID}/completed SetSubmissionCompleted(ctx context.Context, params SetSubmissionCompletedParams) error + // UpdateMapfixDescription implements updateMapfixDescription operation. + // + // Update description (submitter only). + // + // PATCH /mapfixes/{MapfixID}/description + UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error // UpdateMapfixModel implements updateMapfixModel operation. // // Update model following role restrictions. diff --git a/pkg/api/oas_unimplemented_gen.go b/pkg/api/oas_unimplemented_gen.go index ba3bc23..bd924ef 100644 --- a/pkg/api/oas_unimplemented_gen.go +++ b/pkg/api/oas_unimplemented_gen.go @@ -547,6 +547,15 @@ func (UnimplementedHandler) SetSubmissionCompleted(ctx context.Context, params S return ht.ErrNotImplemented } +// UpdateMapfixDescription implements updateMapfixDescription operation. +// +// Update description (submitter only). +// +// PATCH /mapfixes/{MapfixID}/description +func (UnimplementedHandler) UpdateMapfixDescription(ctx context.Context, req UpdateMapfixDescriptionReq, params UpdateMapfixDescriptionParams) error { + return ht.ErrNotImplemented +} + // UpdateMapfixModel implements updateMapfixModel operation. // // Update model following role restrictions. diff --git a/pkg/web_api/mapfixes.go b/pkg/web_api/mapfixes.go index 7232e77..abec5e8 100644 --- a/pkg/web_api/mapfixes.go +++ b/pkg/web_api/mapfixes.go @@ -327,6 +327,48 @@ func (svc *Service) UpdateMapfixModel(ctx context.Context, params api.UpdateMapf ) } +// UpdateMapfixDescription implements updateMapfixDescription operation. +// +// Update description (submitter only, status ChangesRequested or UnderConstruction). +// +// PATCH /mapfixes/{MapfixID}/description +func (svc *Service) UpdateMapfixDescription(ctx context.Context, req api.UpdateMapfixDescriptionReq, params api.UpdateMapfixDescriptionParams) error { + userInfo, ok := ctx.Value("UserInfo").(UserInfoHandle) + if !ok { + return ErrUserInfo + } + + // read mapfix + mapfix, err := svc.inner.GetMapfix(ctx, params.MapfixID) + if err != nil { + return err + } + + userId, err := userInfo.GetUserID() + if err != nil { + return err + } + + // check if caller is the submitter + if userId != mapfix.Submitter { + return ErrPermissionDeniedNotSubmitter + } + + // read the new description from request body + data, err := io.ReadAll(req) + if err != nil { + return err + } + + newDescription := string(data) + + // check if Status is ChangesRequested or UnderConstruction + update := service.NewMapfixUpdate() + update.SetDescription(newDescription) + allow_statuses := []model.MapfixStatus{model.MapfixStatusChangesRequested, model.MapfixStatusUnderConstruction} + return svc.inner.UpdateMapfixIfStatus(ctx, params.MapfixID, allow_statuses, update) +} + // ActionMapfixReject invokes actionMapfixReject operation. // // Role Reviewer changes status from Submitted -> Rejected. diff --git a/web/src/app/_components/comments/AuditEventItem.tsx b/web/src/app/_components/comments/AuditEventItem.tsx index 052549f..bcc2d00 100644 --- a/web/src/app/_components/comments/AuditEventItem.tsx +++ b/web/src/app/_components/comments/AuditEventItem.tsx @@ -20,7 +20,12 @@ export default function AuditEventItem({ event, validatorUser }: AuditEventItemP const { thumbnailUrl, isLoading } = useUserThumbnail(isValidator ? undefined : event.User, '150x150'); return ( - + void; validatorUser: number; userId: number | null; + currentStatus?: number; } export default function CommentsAndAuditSection({ @@ -25,6 +38,7 @@ export default function CommentsAndAuditSection({ handleCommentSubmit, validatorUser, userId, + currentStatus, }: CommentsAndAuditSectionProps) { const [activeTab, setActiveTab] = useState(0); @@ -32,6 +46,16 @@ export default function CommentsAndAuditSection({ setActiveTab(newValue); }; + // Check if there's validator feedback for changes requested status + // Show badge if status is ChangesRequested and there are validator events + const hasValidatorFeedback = currentStatus === 1 && auditEvents.some(event => + event.User === validatorUser && + ( + event.EventType === AuditEventType.Error || + event.EventType === AuditEventType.CheckList + ) + ); + return ( @@ -41,7 +65,24 @@ export default function CommentsAndAuditSection({ aria-label="comments and audit tabs" > - + + Audit Events + {hasValidatorFeedback && ( + + )} + + } + /> diff --git a/web/src/app/_components/review/ReviewButtons.tsx b/web/src/app/_components/review/ReviewButtons.tsx index 2af8273..40c91b8 100644 --- a/web/src/app/_components/review/ReviewButtons.tsx +++ b/web/src/app/_components/review/ReviewButtons.tsx @@ -1,13 +1,16 @@ -import React from 'react'; -import { Button, Stack } from '@mui/material'; +import React, { useState } from 'react'; +import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Box } from '@mui/material'; import {MapfixInfo } from "@/app/ts/Mapfix"; import {hasRole, Roles, RolesConstants} from "@/app/ts/Roles"; import {SubmissionInfo} from "@/app/ts/Submission"; import {Status, StatusMatches} from "@/app/ts/Status"; interface ReviewAction { - name: string, - action: string, + name: string; + action: string; + confirmTitle?: string; + confirmMessage?: string; + requiresConfirmation: boolean; } interface ReviewButtonsProps { @@ -19,20 +22,102 @@ interface ReviewButtonsProps { } const ReviewActions = { - Submit: {name:"Submit",action:"trigger-submit"} as ReviewAction, - AdminSubmit: {name:"Admin Submit",action:"trigger-submit"} as ReviewAction, - SubmitUnchecked: {name:"Submit Unchecked", action:"trigger-submit-unchecked"} as ReviewAction, - ResetSubmitting: {name:"Reset Submitting",action:"reset-submitting"} as ReviewAction, - Revoke: {name:"Revoke",action:"revoke"} as ReviewAction, - Accept: {name:"Accept",action:"trigger-validate"} as ReviewAction, - Reject: {name:"Reject",action:"reject"} as ReviewAction, - Validate: {name:"Validate",action:"retry-validate"} as ReviewAction, - ResetValidating: {name:"Reset Validating",action:"reset-validating"} as ReviewAction, - RequestChanges: {name:"Request Changes",action:"request-changes"} as ReviewAction, - Upload: {name:"Upload",action:"trigger-upload"} as ReviewAction, - ResetUploading: {name:"Reset Uploading",action:"reset-uploading"} as ReviewAction, - Release: {name:"Release",action:"trigger-release"} as ReviewAction, - ResetReleasing: {name:"Reset Releasing",action:"reset-releasing"} as ReviewAction, + Submit: { + name: "Submit for Review", + action: "trigger-submit", + confirmTitle: "Submit for Review", + confirmMessage: "Are you ready to submit this for review? The model version is locked in once submitted, but you can revoke it later if needed.", + requiresConfirmation: true + } as ReviewAction, + AdminSubmit: { + name: "Submit on Behalf of User", + action: "trigger-submit", + confirmTitle: "Admin Submit", + confirmMessage: "This will submit the work as if the original user did it. Continue?", + requiresConfirmation: true + } as ReviewAction, + SubmitUnchecked: { + name: "Approve Without Validation", + action: "trigger-submit-unchecked", + confirmTitle: "Skip Validation", + confirmMessage: "This will approve without running validation checks. Only use this if you're certain the work is correct.", + requiresConfirmation: true + } as ReviewAction, + ResetSubmitting: { + name: "Reset Submit Process", + action: "reset-submitting", + confirmTitle: "Reset Submit", + confirmMessage: "This will force-cancel the submission process and return to 'Under Construction' status. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.", + requiresConfirmation: true + } as ReviewAction, + Revoke: { + name: "Revoke", + action: "revoke", + confirmTitle: "Revoke", + confirmMessage: "This will withdraw from review and return to 'Under Construction' status.", + requiresConfirmation: true + } as ReviewAction, + Accept: { + name: "Accept & Validate", + action: "trigger-validate", + confirmTitle: "Accept", + confirmMessage: "This will accept and trigger validation. The work will proceed to the next stage.", + requiresConfirmation: true + } as ReviewAction, + Reject: { + name: "Reject", + action: "reject", + confirmTitle: "Reject", + confirmMessage: "This will permanently reject. The user will need to create a new one. Are you sure?", + requiresConfirmation: true + } as ReviewAction, + Validate: { + name: "Run Validation", + action: "retry-validate", + requiresConfirmation: false + } as ReviewAction, + ResetValidating: { + name: "Reset Validation Process", + action: "reset-validating", + confirmTitle: "Reset Validation", + confirmMessage: "This will force-abort the validation process so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.", + requiresConfirmation: true + } as ReviewAction, + RequestChanges: { + name: "Request Changes", + action: "request-changes", + confirmTitle: "Request Changes", + confirmMessage: "Request that the submitter make changes. Make sure you've explained which changes are requested in a comment.", + requiresConfirmation: true + } as ReviewAction, + Upload: { + name: "Upload to Roblox", + action: "trigger-upload", + confirmTitle: "Upload to Roblox Group", + confirmMessage: "This will upload the validated work to the Roblox group. Continue?", + requiresConfirmation: true + } as ReviewAction, + ResetUploading: { + name: "Reset Upload Process", + action: "reset-uploading", + confirmTitle: "Reset Upload", + confirmMessage: "This will force-abort the upload to Roblox so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.", + requiresConfirmation: true + } as ReviewAction, + Release: { + name: "Release to Game", + action: "trigger-release", + confirmTitle: "Release to Game", + confirmMessage: "This will make the work available in game. This is the final step!", + requiresConfirmation: true + } as ReviewAction, + ResetReleasing: { + name: "Reset Release Process", + action: "reset-releasing", + confirmTitle: "Reset Release", + confirmMessage: "This will force-abort the release to the game so you can retry. Only use this if you're certain the backend encountered a catastrophic failure. Misuse will corrupt the workflow.", + requiresConfirmation: true + } as ReviewAction, } const ReviewButtons: React.FC = ({ @@ -42,16 +127,46 @@ const ReviewButtons: React.FC = ({ roles, type, }) => { - const getVisibleButtons = () => { - if (!item || userId === null) return []; + const [confirmDialog, setConfirmDialog] = useState<{ + open: boolean; + action: ReviewAction | null; + }>({ open: false, action: null }); + + const handleButtonClick = (action: ReviewAction) => { + if (action.requiresConfirmation) { + setConfirmDialog({ open: true, action }); + } else { + onClick(action.action, item.ID); + } + }; + + const handleConfirm = () => { + if (confirmDialog.action) { + onClick(confirmDialog.action.action, item.ID); + } + setConfirmDialog({ open: false, action: null }); + }; + + const handleCancel = () => { + setConfirmDialog({ open: false, action: null }); + }; + + const getVisibleButtons = () => { + if (!item || userId === null) return { primary: [], secondary: [], submitter: [], reviewer: [], admin: [] }; - // Define a type for the button type ReviewButton = { action: ReviewAction; color: "primary" | "error" | "success" | "info" | "warning"; + variant?: "contained" | "outlined"; + isPrimary?: boolean; }; - const buttons: ReviewButton[] = []; + const primaryButtons: ReviewButton[] = []; + const secondaryButtons: ReviewButton[] = []; + const submitterButtons: ReviewButton[] = []; + const reviewerButtons: ReviewButton[] = []; + const adminButtons: ReviewButton[] = []; + const is_submitter = userId === item.Submitter; const status = item.StatusID; @@ -59,133 +174,215 @@ const ReviewButtons: React.FC = ({ const uploadRole = type === "submission" ? RolesConstants.SubmissionUpload : RolesConstants.MapfixUpload; const releaseRole = type === "submission" ? RolesConstants.SubmissionRelease : RolesConstants.MapfixRelease; + // Submitter actions if (is_submitter) { if (StatusMatches(status, [Status.UnderConstruction, Status.ChangesRequested])) { - buttons.push({ + submitterButtons.push({ action: ReviewActions.Submit, - color: "primary" + color: "success" }); } if (StatusMatches(status, [Status.Submitted, Status.ChangesRequested])) { - buttons.push({ + submitterButtons.push({ action: ReviewActions.Revoke, - color: "error" + color: "warning", + variant: "outlined" }); } if (status === Status.Submitting) { - buttons.push({ + adminButtons.push({ action: ReviewActions.ResetSubmitting, - color: "warning" + color: "error", + variant: "outlined" }); } } - // Buttons for review role + // Reviewer actions if (hasRole(roles, reviewRole)) { if (status === Status.Submitted && !is_submitter) { - buttons.push( - { - action: ReviewActions.Accept, - color: "success" - }, - { - action: ReviewActions.Reject, - color: "error" - } - ); + reviewerButtons.push({ + action: ReviewActions.Accept, + color: "success" + }); + reviewerButtons.push({ + action: ReviewActions.Reject, + color: "error", + variant: "outlined" + }); } if (status === Status.AcceptedUnvalidated) { - buttons.push({ + reviewerButtons.push({ action: ReviewActions.Validate, - color: "info" + color: "primary" }); } if (status === Status.Validating) { - buttons.push({ + adminButtons.push({ action: ReviewActions.ResetValidating, - color: "warning" + color: "error", + variant: "outlined" }); } if (StatusMatches(status, [Status.Validated, Status.AcceptedUnvalidated, Status.Submitted]) && !is_submitter) { - buttons.push({ + reviewerButtons.push({ action: ReviewActions.RequestChanges, - color: "warning" + color: "warning", + variant: "outlined" }); } if (status === Status.ChangesRequested) { - buttons.push({ + adminButtons.push({ action: ReviewActions.SubmitUnchecked, - color: "warning" + color: "warning", + variant: "outlined" }); - // button only exists for submissions - // submitter has normal submit button if (type === "submission" && !is_submitter) { - buttons.push({ + adminButtons.push({ action: ReviewActions.AdminSubmit, - color: "primary" + color: "info", + variant: "outlined" }); } } } - // Buttons for upload role + // Upload role actions if (hasRole(roles, uploadRole)) { if (status === Status.Validated) { - buttons.push({ + reviewerButtons.push({ action: ReviewActions.Upload, color: "success" }); } if (status === Status.Uploading) { - buttons.push({ + adminButtons.push({ action: ReviewActions.ResetUploading, - color: "warning" + color: "error", + variant: "outlined" }); } } - // Buttons for release role + // Release role actions if (hasRole(roles, releaseRole)) { - // submissions do not have a release button if (type === "mapfix" && status === Status.Uploaded) { - buttons.push({ + reviewerButtons.push({ action: ReviewActions.Release, color: "success" }); } if (status === Status.Releasing) { - buttons.push({ + adminButtons.push({ action: ReviewActions.ResetReleasing, - color: "warning" + color: "error", + variant: "outlined" }); } } - return buttons; + return { + primary: primaryButtons, + secondary: secondaryButtons, + submitter: submitterButtons, + reviewer: reviewerButtons, + admin: adminButtons + }; + }; + + const buttons = getVisibleButtons(); + const hasAnyButtons = buttons.submitter.length > 0 || buttons.reviewer.length > 0 || buttons.admin.length > 0; + + if (!hasAnyButtons) return null; + + const ActionCard = ({ title, actions, isFirst = false }: { title: string; actions: any[]; isFirst?: boolean }) => { + if (actions.length === 0) return null; + + return ( + + + {title} + + + {actions.map((button, index) => ( + + ))} + + + ); }; return ( - - {getVisibleButtons().map((button, index) => ( - - ))} - + <> + + + + + + + {/* Confirmation Dialog */} + + + {confirmDialog.action?.confirmTitle || confirmDialog.action?.name} + + + + {confirmDialog.action?.confirmMessage || "Are you sure you want to proceed?"} + + + + + + + + ); }; diff --git a/web/src/app/_components/review/ReviewItem.tsx b/web/src/app/_components/review/ReviewItem.tsx index 19e1ee4..560ce5c 100644 --- a/web/src/app/_components/review/ReviewItem.tsx +++ b/web/src/app/_components/review/ReviewItem.tsx @@ -1,10 +1,15 @@ -import { Paper, Grid, Typography } from "@mui/material"; +import { Paper, Grid, Typography, TextField, IconButton, Box } from "@mui/material"; import { ReviewItemHeader } from "./ReviewItemHeader"; import { CopyableField } from "@/app/_components/review/CopyableField"; import WorkflowStepper from "./WorkflowStepper"; import { SubmissionInfo } from "@/app/ts/Submission"; import { MapfixInfo } from "@/app/ts/Mapfix"; import { getGameName } from "@/app/utils/games"; +import { useState } from "react"; +import EditIcon from '@mui/icons-material/Edit'; +import SaveIcon from '@mui/icons-material/Save'; +import CloseIcon from '@mui/icons-material/Close'; +import { Status, StatusMatches } from "@/app/ts/Status"; // Define a field configuration for specific types interface FieldConfig { @@ -18,12 +23,24 @@ type ReviewItemType = SubmissionInfo | MapfixInfo; interface ReviewItemProps { item: ReviewItemType; handleCopyValue: (value: string) => void; + currentUserId?: number; + userId?: number | null; + onDescriptionUpdate?: () => Promise; + showSnackbar?: (message: string, severity?: 'success' | 'error' | 'info' | 'warning') => void; } export function ReviewItem({ item, - handleCopyValue + handleCopyValue, + currentUserId, + userId, + onDescriptionUpdate, + showSnackbar }: ReviewItemProps) { + const [isEditingDescription, setIsEditingDescription] = useState(false); + const [editedDescription, setEditedDescription] = useState(""); + const [isSaving, setIsSaving] = useState(false); + // Type guard to check if item is valid if (!item) return null; @@ -31,6 +48,57 @@ export function ReviewItem({ const isSubmission = 'UploadedAssetID' in item; const isMapfix = 'TargetAssetID' in item; + // Check if current user is the submitter + const isSubmitter = userId !== null && userId === item.Submitter; + + // Check if description can be edited (only in ChangesRequested or UnderConstruction status) + const canEditDescription = isSubmitter && isMapfix && StatusMatches(item.StatusID, [Status.ChangesRequested, Status.UnderConstruction]); + + const handleEditClick = () => { + setEditedDescription(isMapfix ? (item.Description || "") : ""); + setIsEditingDescription(true); + }; + + const handleCancelEdit = () => { + setIsEditingDescription(false); + setEditedDescription(""); + }; + + const handleSaveDescription = async () => { + if (!isMapfix) return; + + setIsSaving(true); + try { + const response = await fetch(`/v1/mapfixes/${item.ID}/description`, { + method: 'PATCH', + headers: { + 'Content-Type': 'text/plain', + }, + body: editedDescription, + }); + + if (!response.ok) { + throw new Error(`Failed to update description: ${response.status}`); + } + + setIsEditingDescription(false); + if (showSnackbar) { + showSnackbar("Description updated successfully", "success"); + } + if (onDescriptionUpdate) { + await onDescriptionUpdate(); + } + } catch (error) { + console.error("Error updating description:", error); + const errorMessage = error instanceof Error ? error.message : "Failed to update description"; + if (showSnackbar) { + showSnackbar(errorMessage, "error"); + } + } finally { + setIsSaving(false); + } + }; + // Define static fields based on item type let fields: FieldConfig[] = []; if (isSubmission) { @@ -88,14 +156,59 @@ export function ReviewItem({ {/* Description Section */} - {isMapfix && item.Description && ( + {isMapfix && (
- - Description - - - {item.Description} - + + + Description + + {canEditDescription && !isEditingDescription && ( + + + + )} + + {isEditingDescription ? ( + + setEditedDescription(e.target.value)} + placeholder="Describe the changes made in this mapfix" + slotProps={{ htmlInput: { maxLength: 256 } }} + helperText={`${editedDescription.length}/256 characters`} + disabled={isSaving} + /> + + + + + + + + + + ) : ( + + {item.Description || "No description provided"} + + )}
)}
@@ -105,6 +218,8 @@ export function ReviewItem({ diff --git a/web/src/app/_components/review/ReviewItemHeader.tsx b/web/src/app/_components/review/ReviewItemHeader.tsx index 7f2922b..e493e53 100644 --- a/web/src/app/_components/review/ReviewItemHeader.tsx +++ b/web/src/app/_components/review/ReviewItemHeader.tsx @@ -50,7 +50,7 @@ interface ReviewItemHeaderProps { } export const ReviewItemHeader = ({ displayName, assetId, statusId, creator, submitterId }: ReviewItemHeaderProps) => { - const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting]); + const isProcessing = StatusMatches(statusId, [Status.Validating, Status.Uploading, Status.Submitting, Status.Releasing]); const { thumbnailUrl, isLoading } = useUserThumbnail(submitterId, '150x150'); const pulse = keyframes` 0%, 100% { opacity: 0.2; transform: scale(0.8); } diff --git a/web/src/app/_components/review/WorkflowStepper.tsx b/web/src/app/_components/review/WorkflowStepper.tsx index 7c2da15..a1d442d 100644 --- a/web/src/app/_components/review/WorkflowStepper.tsx +++ b/web/src/app/_components/review/WorkflowStepper.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes } from '@mui/material'; +import { Stepper, Step, StepLabel, Box, StepConnector, stepConnectorClasses, StepIconProps, styled, keyframes, Typography, Paper } from '@mui/material'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CancelIcon from '@mui/icons-material/Cancel'; import PendingIcon from '@mui/icons-material/Pending'; import WarningIcon from '@mui/icons-material/Warning'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { Status } from '@/app/ts/Status'; const pulse = keyframes` @@ -18,6 +19,8 @@ const pulse = keyframes` interface WorkflowStepperProps { currentStatus: number; type: 'submission' | 'mapfix'; + submitterId?: number; + currentUserId?: number; } // Define the workflow steps @@ -164,19 +167,49 @@ const CustomStepIcon = (props: StepIconProps & { isRejected?: boolean; isChanges ); }; -const WorkflowStepper: React.FC = ({ currentStatus, type }) => { +const WorkflowStepper: React.FC = ({ currentStatus, type, submitterId, currentUserId }) => { const workflow = type === 'mapfix' ? mapfixWorkflow : submissionWorkflow; // Check if rejected or released const isRejected = currentStatus === Status.Rejected; const isReleased = currentStatus === Status.Release || currentStatus === Status.Releasing; const isChangesRequested = currentStatus === Status.ChangesRequested; + const isUnderConstruction = currentStatus === Status.UnderConstruction; // Find the active step const activeStep = workflow.findIndex(step => step.statuses.includes(currentStatus) ); + // Determine nudge message + const getNudgeContent = () => { + if (isUnderConstruction) { + return { + icon: InfoOutlinedIcon, + title: 'Not Yet Submitted', + message: 'Your submission has been created but has not been submitted. Click "Submit" to submit it.', + color: '#2196f3', + bgColor: 'rgba(33, 150, 243, 0.08)' + }; + } + if (isChangesRequested) { + return { + icon: WarningIcon, + title: 'Changes Requested', + message: 'Review comments and audit events, make modifications, and submit again.', + color: '#ff9800', + bgColor: 'rgba(255, 152, 0, 0.08)' + }; + } + return null; + }; + + const nudge = getNudgeContent(); + + // Only show nudge if current user is the submitter + const isSubmitter = submitterId !== undefined && currentUserId !== undefined && submitterId === currentUserId; + const shouldShowNudge = nudge && isSubmitter; + // If rejected, show all steps as incomplete with error state if (isRejected) { return ( @@ -245,6 +278,36 @@ const WorkflowStepper: React.FC = ({ currentStatus, type } ); })} + + {/* Action Nudge */} + {shouldShowNudge && ( + + + + + + + {nudge.title} + + + {nudge.message} + + + + )}
); }; diff --git a/web/src/app/hooks/useReviewData.ts b/web/src/app/hooks/useReviewData.ts index 41ad71e..503a6b3 100644 --- a/web/src/app/hooks/useReviewData.ts +++ b/web/src/app/hooks/useReviewData.ts @@ -100,7 +100,7 @@ export function useReviewData({itemType, itemId}: UseReviewDataProps): UseReview useEffect(() => { if (data) { - if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating])) { + if (StatusMatches(data.StatusID, [Status.Uploading, Status.Submitting, Status.Validating, Status.Releasing])) { const intervalId = setInterval(() => { fetchData(true); }, 5000); diff --git a/web/src/app/mapfixes/[mapfixId]/page.tsx b/web/src/app/mapfixes/[mapfixId]/page.tsx index 4923c4e..a290bb8 100644 --- a/web/src/app/mapfixes/[mapfixId]/page.tsx +++ b/web/src/app/mapfixes/[mapfixId]/page.tsx @@ -365,6 +365,10 @@ export default function MapfixDetailsPage() { refreshData(true)} + showSnackbar={showSnackbar} /> {/* Comments Section */} @@ -375,6 +379,7 @@ export default function MapfixDetailsPage() { handleCommentSubmit={handleCommentSubmit} validatorUser={validatorUser} userId={user} + currentStatus={mapfix.StatusID} /> diff --git a/web/src/app/submissions/[submissionId]/page.tsx b/web/src/app/submissions/[submissionId]/page.tsx index 84c6c76..e8fc923 100644 --- a/web/src/app/submissions/[submissionId]/page.tsx +++ b/web/src/app/submissions/[submissionId]/page.tsx @@ -267,6 +267,7 @@ export default function SubmissionDetailsPage() { {/* Comments Section */} @@ -277,6 +278,7 @@ export default function SubmissionDetailsPage() { handleCommentSubmit={handleCommentSubmit} validatorUser={validatorUser} userId={user} + currentStatus={submission.StatusID} />