gramene-search 2.13.5 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4862,15 +4862,13 @@ const $12feb7ca48271e17$var$FETCH_FIELDS = [
4862
4862
  ...(0, $4b8473139e48e77e$export$705aff68da3ddba0)
4863
4863
  ])
4864
4864
  ];
4865
+ // Default columns, in display order. The two stress fields are adjacent so the
4866
+ // view can merge them into a single "Activated/Repressed by condition" column.
4865
4867
  const $12feb7ca48271e17$export$e4bc2ac09afbc604 = [
4866
4868
  'id',
4867
- 'name',
4868
- 'system_name',
4869
- 'biotype',
4870
- 'expr_class__attr_ss',
4871
- 'expr_organ_level__attr_ss',
4872
- 'expr_tau__attr_f',
4873
4869
  'expr_max_tpm__attr_f',
4870
+ 'expr_organ_level__attr_ss',
4871
+ 'expr_class__attr_ss',
4874
4872
  'expr_activated_by__attr_ss',
4875
4873
  'expr_repressed_by__attr_ss'
4876
4874
  ];
@@ -16947,9 +16945,12 @@ var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
16947
16945
 
16948
16946
 
16949
16947
  const { MAX_GENES: $8f8e530286a37ce9$var$MAX_GENES } = (0, $12feb7ca48271e17$export$d00a9fd01497b827);
16950
- // expr_organ_level expands into one column per organ (the heatmap block); every
16951
- // other offered field maps to exactly one column.
16948
+ // expr_organ_level expands into one column per organ (the heatmap block).
16952
16949
  const $8f8e530286a37ce9$var$ORGAN_FIELD = 'expr_organ_level__attr_ss';
16950
+ // The two stress fields render as a single merged column.
16951
+ const $8f8e530286a37ce9$var$STRESS_UP = 'expr_activated_by__attr_ss';
16952
+ const $8f8e530286a37ce9$var$STRESS_DOWN = 'expr_repressed_by__attr_ss';
16953
+ const $8f8e530286a37ce9$var$STRESS_COL = 'stress';
16953
16954
  const $8f8e530286a37ce9$var$DEFAULT_COL_DEF = {
16954
16955
  resizable: true,
16955
16956
  sortable: true,
@@ -16971,33 +16972,41 @@ const $8f8e530286a37ce9$var$WIDTHS = {
16971
16972
  expr_tau__attr_f: 80,
16972
16973
  expr_max_tpm__attr_f: 100,
16973
16974
  expr_n_organs_detected__attr_i: 90,
16974
- expr_activated_by__attr_ss: 190,
16975
- expr_repressed_by__attr_ss: 190,
16976
16975
  expr_specific_to__attr_ss: 150,
16977
16976
  expr_enhanced_in__attr_ss: 150,
16978
16977
  expr_high_in__attr_ss: 150
16979
16978
  };
16980
16979
  const $8f8e530286a37ce9$var$joinValues = (p)=>Array.isArray(p.value) ? p.value.map((v)=>String(v).replace(/_/g, ' ')).join(', ') : p.value ?? '';
16981
- // ↑activated / ↓repressed condition chips.
16982
- const $8f8e530286a37ce9$var$chipRenderer = (dir)=>({ value: value })=>{
16983
- const list = Array.isArray(value) ? value : value ? [
16984
- value
16985
- ] : [];
16986
- if (!list.length) return null;
16987
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
16988
- children: list.map((c, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
16989
- style: {
16990
- fontSize: 10,
16991
- lineHeight: '16px',
16992
- padding: '0 4px',
16993
- borderRadius: 2,
16994
- marginRight: 3,
16995
- whiteSpace: 'nowrap',
16996
- background: (0, $4b8473139e48e77e$export$c6dbe9be8c8b382e)[dir].bg,
16997
- color: (0, $4b8473139e48e77e$export$c6dbe9be8c8b382e)[dir].fg
16998
- },
16999
- children: (dir === 'up' ? "\u2191" : "\u2193") + c
17000
- }, i))
16980
+ const $8f8e530286a37ce9$var$StressChip = ({ c: c, dir: dir })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
16981
+ style: {
16982
+ fontSize: 10,
16983
+ lineHeight: '16px',
16984
+ padding: '0 4px',
16985
+ borderRadius: 2,
16986
+ marginRight: 3,
16987
+ whiteSpace: 'nowrap',
16988
+ background: (0, $4b8473139e48e77e$export$c6dbe9be8c8b382e)[dir].bg,
16989
+ color: (0, $4b8473139e48e77e$export$c6dbe9be8c8b382e)[dir].fg
16990
+ },
16991
+ children: (dir === 'up' ? "\u2191" : "\u2193") + c
16992
+ });
16993
+ // Merged ↑activated / ↓repressed renderer; the two directions are shown per the
16994
+ // picker (a column exists if either field is selected).
16995
+ const $8f8e530286a37ce9$var$stressRenderer = (showUp, showDown)=>({ data: data })=>{
16996
+ const up = showUp && Array.isArray(data && data[$8f8e530286a37ce9$var$STRESS_UP]) ? data[$8f8e530286a37ce9$var$STRESS_UP] : [];
16997
+ const down = showDown && Array.isArray(data && data[$8f8e530286a37ce9$var$STRESS_DOWN]) ? data[$8f8e530286a37ce9$var$STRESS_DOWN] : [];
16998
+ if (!up.length && !down.length) return null;
16999
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
17000
+ children: [
17001
+ up.map((c, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($8f8e530286a37ce9$var$StressChip, {
17002
+ c: c,
17003
+ dir: "up"
17004
+ }, `u${i}`)),
17005
+ down.map((c, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($8f8e530286a37ce9$var$StressChip, {
17006
+ c: c,
17007
+ dir: "down"
17008
+ }, `d${i}`))
17009
+ ]
17001
17010
  });
17002
17011
  };
17003
17012
  // Vertical header for the narrow organ columns, so full tissue names stay
@@ -17018,7 +17027,7 @@ const $8f8e530286a37ce9$var$RotatedHeader = (props)=>{
17018
17027
  props.column
17019
17028
  ]);
17020
17029
  const onClick = (e)=>props.progressSort && props.progressSort(e.shiftKey);
17021
- const arrow = sort === 'asc' ? "\u2192\u2009" : sort === 'desc' ? "\u2190\u2009" : '';
17030
+ const arrow = sort === 'asc' ? "\u2192 " : sort === 'desc' ? "\u2190 " : '';
17022
17031
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
17023
17032
  className: "attrtable-rot-header",
17024
17033
  title: props.displayName,
@@ -17032,9 +17041,11 @@ const $8f8e530286a37ce9$var$RotatedHeader = (props)=>{
17032
17041
  });
17033
17042
  };
17034
17043
  const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
17035
- const { attrTable: attrTable, fieldCatalog: fieldCatalog, fieldCatalogByName: fieldCatalogByName, doToggleAttrTableField: doToggleAttrTableField, doBulkSetAttrTableFields: doBulkSetAttrTableFields, doResetAttrTableFields: doResetAttrTableFields } = props;
17044
+ const { attrTable: attrTable, fieldCatalog: fieldCatalog, fieldCatalogByName: fieldCatalogByName, doToggleAttrTableField: doToggleAttrTableField, doBulkSetAttrTableFields: doBulkSetAttrTableFields, doResetAttrTableFields: doResetAttrTableFields, doAcceptGrameneSuggestion: doAcceptGrameneSuggestion } = props;
17036
17045
  const [showColumns, setShowColumns] = (0, $gXNCa$react.useState)(false);
17037
17046
  const [fieldQuery, setFieldQuery] = (0, $gXNCa$react.useState)('');
17047
+ const [popover, setPopover] = (0, $gXNCa$react.useState)(null);
17048
+ const popRef = (0, $gXNCa$react.useRef)(null);
17038
17049
  const { docs: docs, total: total, truncated: truncated, status: status, error: error, visibleFields: visibleFields } = attrTable;
17039
17050
  const visibleSet = (0, $gXNCa$react.useMemo)(()=>new Set(visibleFields), [
17040
17051
  visibleFields
@@ -17069,46 +17080,33 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
17069
17080
  }, [
17070
17081
  docs
17071
17082
  ]);
17072
- // Only the Core identifiers + Expression attributes groups are offered, in
17073
- // catalog order. Falls back to the built-in lists until the catalog loads.
17074
- const orderedFields = (0, $gXNCa$react.useMemo)(()=>{
17075
- const groups = fieldCatalog && fieldCatalog.groups || [];
17076
- const out = [];
17077
- (0, $12feb7ca48271e17$export$53e90de8d659db95).forEach((gid)=>{
17078
- const g = groups.find((x)=>x.id === gid);
17079
- if (g && g.fields) out.push(...g.fields);
17080
- });
17081
- return out.length ? out : [
17082
- ...(0, $12feb7ca48271e17$export$9d77d82cc773e55e),
17083
- ...(0, $4b8473139e48e77e$export$705aff68da3ddba0)
17084
- ];
17085
- }, [
17086
- fieldCatalog
17087
- ]);
17083
+ // Only the Core identifiers + Expression attributes groups are offered.
17088
17084
  const pickerCatalog = (0, $gXNCa$react.useMemo)(()=>{
17089
17085
  if (!fieldCatalog || !fieldCatalog.groups) return null;
17090
17086
  return {
17091
17087
  ...fieldCatalog,
17092
- groups: fieldCatalog.groups.filter((g)=>(0, $12feb7ca48271e17$export$53e90de8d659db95).includes(g.id))
17088
+ groups: fieldCatalog.groups.filter((g)=>[
17089
+ 'core',
17090
+ 'exprattrs'
17091
+ ].includes(g.id))
17093
17092
  };
17094
17093
  }, [
17095
17094
  fieldCatalog
17096
17095
  ]);
17096
+ const labelOf = (f)=>fieldCatalogByName && fieldCatalogByName[f] && fieldCatalogByName[f].label || f;
17097
+ // Columns follow the selected-field order (so the default order is honoured),
17098
+ // expanding organ_level into the heatmap block and merging the two stress
17099
+ // fields into one column.
17097
17100
  const columnDefs = (0, $gXNCa$react.useMemo)(()=>{
17098
- const labelOf = (f)=>fieldCatalogByName && fieldCatalogByName[f] && fieldCatalogByName[f].label || f;
17099
17101
  const cols = [];
17100
- orderedFields.forEach((f)=>{
17101
- if (!visibleSet.has(f)) return;
17102
- const headerName = labelOf(f);
17103
- const width = $8f8e530286a37ce9$var$WIDTHS[f] || 150;
17102
+ let stressDone = false;
17103
+ visibleFields.forEach((f)=>{
17104
17104
  if (f === $8f8e530286a37ce9$var$ORGAN_FIELD) {
17105
17105
  organs.forEach((o, i)=>{
17106
- // The last organ keeps a right border (heatmap's outer edge); the rest
17107
- // drop it so the heatmap reads as one continuous block.
17108
17106
  const last = i === organs.length - 1 ? ' attrtable-organ-last' : '';
17109
17107
  cols.push({
17110
- // No ':' in colId — ag-grid uses colId in internal CSS selectors, where
17111
- // a colon is a metacharacter and silently breaks the column.
17108
+ // No ':' in colId — ag-grid uses colId in internal CSS selectors,
17109
+ // where a colon is a metacharacter and silently breaks the column.
17112
17110
  colId: `organ_${o}`,
17113
17111
  headerName: (0, $4b8473139e48e77e$export$50c1f966fd3af83e)(o),
17114
17112
  headerComponent: $8f8e530286a37ce9$var$RotatedHeader,
@@ -17138,12 +17136,25 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
17138
17136
  });
17139
17137
  return;
17140
17138
  }
17139
+ if (f === $8f8e530286a37ce9$var$STRESS_UP || f === $8f8e530286a37ce9$var$STRESS_DOWN) {
17140
+ if (stressDone) return;
17141
+ stressDone = true;
17142
+ cols.push({
17143
+ colId: $8f8e530286a37ce9$var$STRESS_COL,
17144
+ headerName: 'Activated/Repressed by condition',
17145
+ width: 240,
17146
+ sortable: false,
17147
+ valueGetter: ()=>'',
17148
+ cellRenderer: $8f8e530286a37ce9$var$stressRenderer(visibleSet.has($8f8e530286a37ce9$var$STRESS_UP), visibleSet.has($8f8e530286a37ce9$var$STRESS_DOWN))
17149
+ });
17150
+ return;
17151
+ }
17141
17152
  if (f === 'expr_max_tpm__attr_f') {
17142
17153
  cols.push({
17143
17154
  colId: f,
17144
17155
  field: f,
17145
- headerName: headerName,
17146
- width: width,
17156
+ headerName: labelOf(f),
17157
+ width: $8f8e530286a37ce9$var$WIDTHS[f] || 100,
17147
17158
  type: 'numericColumn',
17148
17159
  valueFormatter: (p)=>(0, $4b8473139e48e77e$export$8eaa32ef86afdd9c)(p.value),
17149
17160
  cellStyle: (p)=>({
@@ -17156,43 +17167,153 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
17156
17167
  cols.push({
17157
17168
  colId: f,
17158
17169
  field: f,
17159
- headerName: headerName,
17160
- width: width,
17170
+ headerName: labelOf(f),
17171
+ width: $8f8e530286a37ce9$var$WIDTHS[f] || 80,
17161
17172
  type: 'numericColumn',
17162
17173
  valueFormatter: (p)=>Number.isFinite(p.value) ? p.value.toFixed(3) : ''
17163
17174
  });
17164
17175
  return;
17165
17176
  }
17166
- if (f === 'expr_activated_by__attr_ss' || f === 'expr_repressed_by__attr_ss') {
17167
- cols.push({
17168
- colId: f,
17169
- field: f,
17170
- headerName: headerName,
17171
- width: width,
17172
- sortable: false,
17173
- valueFormatter: ()=>'',
17174
- cellRenderer: $8f8e530286a37ce9$var$chipRenderer(f === 'expr_activated_by__attr_ss' ? 'up' : 'down')
17175
- });
17176
- return;
17177
- }
17178
17177
  cols.push({
17179
17178
  colId: f,
17180
17179
  field: f,
17181
- headerName: headerName,
17180
+ headerName: labelOf(f),
17182
17181
  headerTooltip: f,
17183
- width: width,
17182
+ width: $8f8e530286a37ce9$var$WIDTHS[f] || 150,
17184
17183
  pinned: f === 'id' || f === 'name' ? 'left' : undefined,
17185
17184
  valueFormatter: $8f8e530286a37ce9$var$joinValues
17186
17185
  });
17187
17186
  });
17188
17187
  return cols;
17188
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17189
17189
  }, [
17190
- orderedFields,
17190
+ visibleFields,
17191
17191
  visibleSet,
17192
17192
  organs,
17193
17193
  tpmRange,
17194
17194
  fieldCatalogByName
17195
17195
  ]);
17196
+ // Build the click-popover for a cell: a title (the column meaning) and one
17197
+ // filterable item per value. Each item carries the fq_field / fq_value the
17198
+ // "add filter" button hands to doAcceptGrameneSuggestion.
17199
+ const buildCellPopover = (colId, data)=>{
17200
+ if (!data) return null;
17201
+ const mk = (text, fqField, fqValue, category, name)=>({
17202
+ text: text,
17203
+ fqField: fqField,
17204
+ fqValue: fqValue,
17205
+ category: category,
17206
+ name: name || text
17207
+ });
17208
+ if (colId === 'id') return {
17209
+ title: 'Gene ID',
17210
+ items: [
17211
+ mk(data.id, 'id', data.id, 'Gene', data.id)
17212
+ ]
17213
+ };
17214
+ if (colId === $8f8e530286a37ce9$var$STRESS_COL) {
17215
+ const items = [];
17216
+ (data[$8f8e530286a37ce9$var$STRESS_UP] || []).forEach((c)=>items.push(mk("\u2191 " + c, $8f8e530286a37ce9$var$STRESS_UP, c, 'Activated by', 'activated by ' + c)));
17217
+ (data[$8f8e530286a37ce9$var$STRESS_DOWN] || []).forEach((c)=>items.push(mk("\u2193 " + c, $8f8e530286a37ce9$var$STRESS_DOWN, c, 'Repressed by', 'repressed by ' + c)));
17218
+ return {
17219
+ title: 'Activated/Repressed by condition',
17220
+ items: items
17221
+ };
17222
+ }
17223
+ if (colId.startsWith('organ_')) {
17224
+ const organ = colId.slice(6);
17225
+ const lvl = data._organ && data._organ[organ];
17226
+ if (!lvl) return {
17227
+ title: 'Expression level by organ',
17228
+ items: []
17229
+ };
17230
+ const label = `${(0, $4b8473139e48e77e$export$50c1f966fd3af83e)(organ)}: ${(0, $4b8473139e48e77e$export$433053e2c141d410)[lvl] || lvl}`;
17231
+ return {
17232
+ title: 'Expression level by organ',
17233
+ items: [
17234
+ mk(label, $8f8e530286a37ce9$var$ORGAN_FIELD, `${organ}:${lvl}`, 'Expression', label)
17235
+ ]
17236
+ };
17237
+ }
17238
+ if (colId === 'expr_max_tpm__attr_f') {
17239
+ const v = data.expr_max_tpm__attr_f;
17240
+ if (!Number.isFinite(+v)) return {
17241
+ title: labelOf(colId),
17242
+ items: []
17243
+ };
17244
+ return {
17245
+ title: labelOf(colId),
17246
+ items: [
17247
+ mk(`\u{2265} ${(0, $4b8473139e48e77e$export$8eaa32ef86afdd9c)(+v)} TPM`, colId, `[${v} TO *]`, 'Max TPM', `Max TPM \u{2265} ${(0, $4b8473139e48e77e$export$8eaa32ef86afdd9c)(+v)}`)
17248
+ ]
17249
+ };
17250
+ }
17251
+ if (colId === 'expr_tau__attr_f') {
17252
+ const v = data.expr_tau__attr_f;
17253
+ if (!Number.isFinite(+v)) return {
17254
+ title: labelOf(colId),
17255
+ items: []
17256
+ };
17257
+ return {
17258
+ title: labelOf(colId),
17259
+ items: [
17260
+ mk(`\u{2265} ${(+v).toFixed(3)}`, colId, `[${v} TO *]`, 'Tau', `tau \u{2265} ${(+v).toFixed(3)}`)
17261
+ ]
17262
+ };
17263
+ }
17264
+ // Generic (Expression class, and any core / added attribute).
17265
+ const label = labelOf(colId);
17266
+ const raw = data[colId];
17267
+ const vals = Array.isArray(raw) ? raw : raw != null && raw !== '' ? [
17268
+ raw
17269
+ ] : [];
17270
+ return {
17271
+ title: label,
17272
+ items: vals.map((v)=>mk(String(v).replace(/_/g, ' '), colId, v, label, String(v).replace(/_/g, ' ')))
17273
+ };
17274
+ };
17275
+ const onCellClicked = (e)=>{
17276
+ const desc = buildCellPopover(e.column.getColId(), e.data);
17277
+ if (!desc || !desc.items.length) {
17278
+ setPopover(null);
17279
+ return;
17280
+ }
17281
+ const ev = e.event || {};
17282
+ setPopover({
17283
+ x: ev.clientX || 0,
17284
+ y: ev.clientY || 0,
17285
+ title: desc.title,
17286
+ items: desc.items
17287
+ });
17288
+ };
17289
+ const addFilter = (it)=>{
17290
+ doAcceptGrameneSuggestion({
17291
+ fq_field: it.fqField,
17292
+ fq_value: it.fqValue,
17293
+ name: it.name,
17294
+ category: it.category
17295
+ });
17296
+ setPopover(null);
17297
+ };
17298
+ // Close the popover on outside-click or Escape. Registered after the opening
17299
+ // click, so that click can't immediately dismiss it.
17300
+ (0, $gXNCa$react.useEffect)(()=>{
17301
+ if (!popover) return;
17302
+ const onDown = (e)=>{
17303
+ if (popRef.current && !popRef.current.contains(e.target)) setPopover(null);
17304
+ };
17305
+ const onKey = (e)=>{
17306
+ if (e.key === 'Escape') setPopover(null);
17307
+ };
17308
+ document.addEventListener('mousedown', onDown, true);
17309
+ document.addEventListener('keydown', onKey);
17310
+ return ()=>{
17311
+ document.removeEventListener('mousedown', onDown, true);
17312
+ document.removeEventListener('keydown', onKey);
17313
+ };
17314
+ }, [
17315
+ popover
17316
+ ]);
17196
17317
  const shown = rows.length;
17197
17318
  const loading = status === 'loading';
17198
17319
  const organShown = visibleSet.has($8f8e530286a37ce9$var$ORGAN_FIELD);
@@ -17321,13 +17442,43 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
17321
17442
  suppressDragLeaveHidesColumns: true,
17322
17443
  tooltipShowDelay: 300,
17323
17444
  rowHeight: 22,
17324
- headerHeight: organShown ? 92 : 28
17445
+ headerHeight: organShown ? 92 : 28,
17446
+ onCellClicked: onCellClicked
17325
17447
  })
17448
+ }),
17449
+ popover && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
17450
+ ref: popRef,
17451
+ className: "attrtable-popover",
17452
+ style: {
17453
+ left: Math.min(popover.x, window.innerWidth - 280),
17454
+ top: Math.min(popover.y, window.innerHeight - 32 - popover.items.length * 30)
17455
+ },
17456
+ children: [
17457
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
17458
+ className: "attrtable-popover-title",
17459
+ children: popover.title
17460
+ }),
17461
+ popover.items.map((it, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
17462
+ className: "attrtable-popover-row",
17463
+ children: [
17464
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
17465
+ className: "attrtable-popover-val",
17466
+ children: it.text
17467
+ }),
17468
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
17469
+ type: "button",
17470
+ className: "btn btn-sm btn-outline-primary attrtable-popover-btn",
17471
+ onClick: ()=>addFilter(it),
17472
+ children: "add filter"
17473
+ })
17474
+ ]
17475
+ }, i))
17476
+ ]
17326
17477
  })
17327
17478
  ]
17328
17479
  });
17329
17480
  };
17330
- var $8f8e530286a37ce9$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectAttrTable', 'selectFieldCatalog', 'selectFieldCatalogByName', 'doToggleAttrTableField', 'doBulkSetAttrTableFields', 'doResetAttrTableFields', $8f8e530286a37ce9$var$AttrTableViewCmp);
17481
+ var $8f8e530286a37ce9$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectAttrTable', 'selectFieldCatalog', 'selectFieldCatalogByName', 'doToggleAttrTableField', 'doBulkSetAttrTableFields', 'doResetAttrTableFields', 'doAcceptGrameneSuggestion', $8f8e530286a37ce9$var$AttrTableViewCmp);
17331
17482
 
17332
17483
 
17333
17484