{"record":{"id":"dd34bac9a59850d4","repo":"siyuan-note/siyuan","slug":"at-least-one-visible-view-is-required","errorCode":null,"errorMessage":"at least one visible view is required","messagePattern":"at least one visible view is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/attribute_view.go","lineNumber":1386,"sourceCode":"func (tx *Transaction) doSetAttrViewBlockView(operation *Operation) (ret *TxErr) {\n\terr := SetDatabaseBlockView(operation.BlockID, operation.AvID, operation.ID)\n\tif err != nil {\n\t\treturn &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}\n\t}\n\treturn\n}\n\nfunc (tx *Transaction) doSetAttrViewBlockVisibleViews(operation *Operation) (ret *TxErr) {\n\terr := SetDatabaseBlockVisibleViews(operation.BlockID, operation.AvID, operation.ViewIDs)\n\tif err != nil {\n\t\treturn &TxErr{code: TxErrHandleAttributeView, id: operation.AvID, msg: err.Error()}\n\t}\n\treturn\n}\n\nfunc SetDatabaseBlockVisibleViews(blockID, avID string, viewIDs []string) (err error) {\n\tif 1 > len(viewIDs) {\n\t\treturn errors.New(\"at least one visible view is required\")\n\t}\n\n\tattrView, err := av.ParseAttributeView(avID)\n\tif nil != err {\n\t\tlogging.LogErrorf(\"parse attribute view [%s] failed: %s\", avID, err)\n\t\treturn\n\t}\n\n\tvisible := map[string]bool{}\n\tfor _, viewID := range viewIDs {\n\t\tif nil == attrView.GetView(viewID) {\n\t\t\treturn fmt.Errorf(\"view [%s] not found in attribute view [%s]\", viewID, avID)\n\t\t}\n\t\tvisible[viewID] = true\n\t}\n\n\tvar normalized []string\n\tfor _, view := range attrView.Views {","sourceCodeStart":1368,"sourceCodeEnd":1404,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/attribute_view.go#L1368-L1404","documentation":"SetDatabaseBlockVisibleViews requires at least one view ID in the viewIDs list; an empty list is rejected before the attribute view is even parsed. A database block must always keep at least one visible view so the block remains renderable.","triggerScenarios":"Calling SetDatabaseBlockVisibleViews (kernel API, via doSetAttrViewBlockVisibleViews) with viewIDs = [] or nil.","commonSituations":"Frontend hides views one by one and accidentally hides the last one; plugin computes visible view IDs from a filter that matched nothing; serialization bug dropping empty arrays.","solutions":["Always include at least one existing view ID in viewIDs; if hiding the last view, reset to all view IDs or keep a default view visible.","On the caller side, check viewIDs.length >= 1 before invoking the API.","If the intent is to delete a view rather than hide it, use the view-delete operation instead of visible-view setting."],"exampleFix":"// before\nsetVisibleViews(avID, []);\n// after\nconst ids = computeVisibleViews();\nsetVisibleViews(avID, ids.length ? ids : [getDefaultViewId(avID)]);","handlingStrategy":"validation","validationCode":"if (!Array.isArray(viewIDs) || viewIDs.length < 1) throw new Error('at least one visible view id required');","typeGuard":"const isNonEmptyStringArray = (v: unknown): v is string[] => Array.isArray(v) && v.length > 0 && v.every(x => typeof x === 'string');","tryCatchPattern":"try { await setVisibleViews(blockID, avID, viewIDs); } catch (e) { if (String(e).includes('at least one visible view')) { await setVisibleViews(blockID, avID, allViewIds); } else throw e; }","preventionTips":["Never compute visibility from an empty filter result without a fallback","Keep a default view always visible when hiding views","Treat 'hide last view' UI action as 'reset to all views'"],"tags":["attribute-view","empty-list","validation"],"backgroundTag":"empty-required-field","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}